有关本书和其他 Manning 书籍的在线信息和订购,请访问www.manning.com。出版商在订购大量本书时提供折扣。获取更多资讯,请联系
For online information and ordering of this and other Manning books, please visit www.manning.com. The publisher offers discounts on this book when ordered in quantity. For more information, please contact
特约营业部 曼宁出版公司鲍德温路 20 号 邮政信箱 761 纽约州庇护岛 11964 邮箱: orders@manning.com
Special Sales Department Manning Publications Co. 20 Baldwin Road PO Box 761 Shelter Island, NY 11964 Email: orders@manning.com
©2020 Manning Publications Co. 保留所有权利。
©2020 by Manning Publications Co. All rights reserved.
未经出版商事先书面许可,不得以任何形式或通过电子、机械、影印或其他方式复制、存储在检索系统中或传播本出版物的任何部分。
No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by means electronic, mechanical, photocopying, or otherwise, without prior written permission of the publisher.
制造商和销售商用来区分其产品的许多名称都被声明为商标。如果这些名称出现在书中,并且 Manning Publications 知道商标声明,则这些名称已印在首字母大写或全部大写中。
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in the book, and Manning Publications was aware of a trademark claim, the designations have been printed in initial caps or all caps.
认识到保存所写内容的重要性,Manning 的政策是将我们出版的书籍印刷在无酸纸上,我们为此尽最大努力。还认识到我们有责任保护地球资源,Manning 书籍印刷的纸张至少有 15% 被回收利用,并且在不使用元素氯的情况下进行加工。
Recognizing the importance of preserving what has been written, it is Manning’s policy to have the books we publish printed on acid-free paper, and we exert our best efforts to that end. Recognizing also our responsibility to conserve the resources of our planet, Manning books are printed on paper that is at least 15 percent recycled and processed without the use of elemental chlorine.
曼宁出版公司鲍德温路 20 号 邮政信箱 761 纽约州庇护岛 11964 Manning Publications Co.20 Baldwin Road PO Box 761 Shelter Island, NY 11964 |
开发编辑:Elesha Hyde 技术开发编辑:Mike Shepard 评论编辑:Aleksandar Dragosavljević 项目经理:Lori Weidert 文案编辑:凯西·辛普森 校对:Melody Dolab 技术校对:German Gonzalez-Morris 排版和封面设计师:Marija Tudor
Development editor: Elesha Hyde Technical development editor: Mike Shepard Review editor: Aleksandar Dragosavljević Project manager: Lori Weidert Copy editor: Kathy Simpson Proofreader: Melody Dolab Technical proofreader: German Gonzalez-Morris Typesetter and cover designer: Marija Tudor
书号 9781617296413
ISBN 9781617296413
美国印制
Printed in the United States of America
Chapter 1. Introduction to typing
Chapter 6. Advanced applications of function types
Chapter 8. Elements of object-oriented programming
Chapter 9. Generic data structures
Chapter 10. Generic algorithms and iterators
Chapter 11. Higher kinded types and beyond
10.1. 更好的 map()、filter()、reduce()
Chapter 1. Introduction to typing
2.1. Designing functions that don’t return values
2.2. Boolean logic and short circuits
2.3. Common pitfalls of numerical types
2.3.1. Integer types and overflow
2.3.2. Floating-point types and rounding
2.5. Building data structures with arrays and references
Designing functions that don’t return values
Boolean logic and short circuits
3.2. Expressing either-or with types
4.1. Avoiding primitive obsession to prevent misinterpretation
4.1.1. The Mars Climate Orbiter
4.2.1. Enforcing constraints with the constructor
4.4. Hiding and restoring type information
5.1. A simple strategy pattern
5.2. A state machine without switch statements
5.2.1. Early Programming with Types
5.3. Avoiding expensive computation with lazy values
5.4. Using map, filter, and reduce
A state machine without switch statements
Chapter 6. Advanced applications of function types
6.1. A simple decorator pattern
6.2.1. An object-oriented counter
6.3. Executing long-running operations asynchronously
6.3.2. Asynchronous execution: callbacks
6.3.3. Asynchronous execution models
6.4. Simplifying asynchronous code
7.1. Distinguishing between similar types in TypeScript
7.1.1. Structural and nominal subtyping pros and cons
7.2. Assigning anything to, assigning to anything
7.3.1. Subtyping and sum types
7.3.2. Subtyping and collections
7.3.3. Subtyping and function return types
Distinguishing between similar types in TypeScript
Chapter 8. Elements of object-oriented programming
8.1. Defining contracts with interfaces
8.2. Inheriting data and behavior
8.3. Composing data and behavior
8.3.1. The has-a rule of thumb
8.4. Extending data and behavior
8.4.1. Extending behavior with composition
8.5. Alternatives to purely object-oriented code
Chapter 9. Generic data structures
9.2.1. Generic data structures
9.3. Traversing any data structure
Chapter 10. Generic algorithms and iterators
10.1. Better map(), filter(), reduce()
10.2.1. Algorithms instead of loops
10.3. Constraining type parameters
10.3.1. Generic data structures with type constraints
10.4. Efficient reverse and other algorithms using iterators
10.4.1. Iterator building blocks
10.4.3. An efficient reverse()
Better map(), filter(), reduce()
Chapter 11. Higher kinded types and beyond
11.1. An even more general map
11.1.1. Processing results or propagating errors
11.1.2. Mix-and-match function application
11.1.3. Functors and higher kinded types
11.2.2. Difference between map() and bind()
11.3.1. Functional programming
A. TypeScript installation and source code
Programming with Types是多年学习类型系统和软件正确性的结晶,提炼成一本具有实际应用程序的实用书籍。
Programming with Types is the culmination of multiple years of learning about type systems and software correctness, distilled into a practical book with real-world applications.
我一直喜欢学习如何编写更好的代码,但如果我要准确指出我是何时开始走这条路的,我会说是 2015 年。那时我正在更换团队并想加快速度现代 C++。我开始观看 C++ 会议视频,拿起 Alexander Stepanov 关于泛型编程的书籍,对如何编写代码有了完全不同的看法。
I’ve always liked learning how to write better code, but if I were to point out exactly when I started down this path, I’d say it was 2015. I was switching teams at that point and wanted to get up to speed on modern C++. I started watching C++ conference videos, picked up Alexander Stepanov’s books on generic programming, and gained a completely different perspective on how to write code.
同时,我在业余时间学习 Haskell,并逐步了解其类型系统的高级功能。使用函数式语言进行编程可以清楚地看到,随着时间的推移,这些语言中理所当然的一些特性如何被更多的主流语言所采用。
In parallel, I was learning Haskell in my spare time and working my way through the advanced features of its type system. Programming in a functional language makes it obvious how some of the features taken for granted in such languages get adopted by more mainstream languages as time goes by.
我读了几本关于这个主题的书,从 Stepanov 的《编程基础》和《从数学到泛型编程》到 Bartosz Milewski 的《程序员范畴论》和 Benjamin Pierce 的《类型和编程语言》。正如您可能从标题中看出的那样,这些书更多的是在理论/数学方面。在更多地了解类型系统的同时,我可以看出我在工作中编写的代码变得更好了。类型系统设计的更多理论领域与日常生产软件之间存在直接联系。这不是一个革命性的发现:奇特的类型系统特性是为了解决现实世界的问题而存在的。
I read several books on the topic, from Stepanov’s Elements of Programming and From Mathematics to Generic Programming to Bartosz Milewski’s Category Theory for Programmers and Benjamin Pierce’s Types and Programming Languages. As you might be able to tell from the titles, these books are more on the theoretical/mathematical side. While learning more about type systems, I could tell that the code I was writing at work became better. There is a direct link between the more theoretical realm of type system design and the day-to-day production software. This isn’t a revolutionary discovery: fancy type system features exist to address real-world problems.
我意识到并不是每个实践中的程序员都有时间和耐心阅读带有数学证明的厚书。另一方面,我的时间并没有浪费在读这些书上:它们让我成为了更好的软件工程师。我认为有足够的空间来介绍类型系统及其提供的更非正式的好处,重点关注任何人在日常工作中都可以使用的实际应用程序。
I realized that not every practicing programmer has the time and patience to read dense books with mathematical proofs. On the other hand, my time wasn’t wasted reading such books: they made me a better software engineer. I figured there is room for a book that covers type systems and the benefits they provide more informally, focusing on practical applications anyone can use in their day job.
Programming with Types旨在提供从基本类型开始的类型系统特性的演练,涵盖函数类型和子类型、OOP、泛型编程以及更高种类的类型,如仿函数和单子。我没有关注这些特性背后的理论,而是根据实际应用来描述它们中的每一个。本书展示了如何以及何时使用这些功能中的每一个来改进您的代码。
Programming with Types aims to provide a walk-through of type system features starting from basic types, covering function types and subtyping, OOP, generic programming, and higher kinded types such as functors and monads. Instead of focusing on the theory behind these features, I describe each one of them in terms of practical applications. The book shows how and when to use each of these features to improve your code.
代码示例最初应该使用 C++。C++ 类型系统比 Java 和 C# 等语言功能强大且功能更丰富。另一方面,C++ 是一门复杂的语言,我不想限制本书的读者,所以我决定改用 TypeScript。TypeScript 也有一个强大的类型系统,但它的语法更容易理解,所以即使你来自另一种语言,它也应该很容易理解大多数例子。附录 B 提供了本书中使用的 TypeScript 子集的快速备忘单。
The code samples were originally supposed to be in C++. The C++ type system is powerful and more feature-rich than languages such as Java and C#. On the other hand, C++ is a complex language, and I didn’t want to limit the audience of the book, so I decided to use TypeScript instead. TypeScript has a powerful type system too, but its syntax is more accessible, so it should be easy to work through most examples even if you’re coming from another language. Appendix B provides a quick cheat sheet for the subset of TypeScript used in this book.
我希望您喜欢阅读本书并学习一些可以立即应用于您的项目的新技术。
I hope you enjoy reading this book and learn some new techniques that you can apply to your projects right away.
首先,我要感谢我的家人对我的支持和理解。我的妻子戴安娜和女儿艾达一路陪伴着我,给了我完成本书所需的所有鼓励和空间。
First, I want to thank my family for their support and understanding. My wife, Diana, and my daughter, Ada, were with me every step of the way, giving me all the encouragement and space I needed to complete this book.
写书绝对是团队的努力。我很感谢迈克尔·斯蒂芬斯 (Michael Stephens) 的初步反馈,正是这些反馈使这本书成为您今天所读的内容。我要感谢我的编辑 Elesha Hyde,感谢她提供的所有帮助、建议和反馈。感谢 Mike Shepard 审阅每一章并让我保持诚实。另外,感谢 German Gonzales 仔细检查每个代码示例并确保一切都按照描述进行。我要感谢所有审稿人花时间并提供宝贵的反馈。感谢 Viktor Bek、Roberto Casadei、Ahmed Chicktay、John Corley、Justin Coulston、Theo Despoudis、David DiMaria、Christopher Fry、German Gonzalez-Morris、Vipul Gupta、Peter Hampton、Clive Harber、Fred Heath、Ryan Huber、Des Horsley、Kevin诺曼·D·卡普昌,
Writing a book is most definitely a team effort. I’m grateful for Michael Stephens’ initial feedback, which helped shape the book into what you are reading today. I want to thank my editor, Elesha Hyde, for all her help, advice, and feedback. Thanks to Mike Shepard for reviewing every chapter and keeping me honest. Also, thanks to German Gonzales for going through each and every code sample and making sure that everything works as described. I want to thank all reviewers for taking their time and providing invaluable feedback. Thanks to Viktor Bek, Roberto Casadei, Ahmed Chicktay, John Corley, Justin Coulston, Theo Despoudis, David DiMaria, Christopher Fry, German Gonzalez-Morris, Vipul Gupta, Peter Hampton, Clive Harber, Fred Heath, Ryan Huber, Des Horsley, Kevin Norman D. Kapchan, Jose San Leandro, James Liu, Wayne Mather, Arnaldo Gabriel Ayala Meyer, Riccardo Noviello, Marco Perone, Jermal Prestwood, Borja Quevedo, Domingo Sebastián Sastre, Rohit Sharm, and Greg Wright.
我要感谢我的同事和导师,感谢他们教给我的一切。当我学习如何利用类型来改进我们的代码库时,我很幸运有一些很棒的、支持我的经理。感谢 Mike Navarro、David Hansen 和 Ben Ross 的信任。
I want to thank my colleagues and mentors for everything they taught me. As I was learning about leveraging types to improve our codebase, I was lucky to have some great, supportive managers. Thanks to Mike Navarro, David Hansen, and Ben Ross for your trust.
感谢整个 C++ 社区,我从中学到了很多东西,尤其要感谢 Sean Parent 鼓舞人心的演讲和宝贵建议。
Thanks to the whole C++ community from which I learned so much and especially to Sean Parent for his inspiring talks and his great advice.
Programming with Types旨在展示如何使用类型系统来编写更好、更安全的代码。尽管大多数讨论类型系统的书籍都侧重于更正式的方面,但本书采用了务实的方法。它包含您在日常工作中会遇到的大量示例、应用程序和场景。
Programming with Types aims to show how you can use type systems to write better, safer code. Although most books discussing type systems focus on more formal aspects, this book takes a pragmatic approach. It contains numerous examples, applications, and scenarios that you will encounter in your day job.
本书适用于希望更多地了解类型系统如何工作以及如何使用它们来提高代码质量的实践程序员。您应该具有使用面向对象语言(如 Java、C#、C++ 或 JavaScript/TypeScript)的经验。您还应该具备一些最低限度的软件设计经验。尽管本书将提供各种用于编写健壮、可组合且封装更好的代码的技术,但它假定您知道为什么需要这些属性。
This book is for practicing programmers who want to learn more about how type systems work and how to use them to improve code quality. You should have some experience using an object-oriented language such as Java, C#, C++, or JavaScript/ TypeScript. You should also have some minimum software design experience. Although the book will provide various techniques for writing robust, composable, and better-encapsulated code, it assumes that you know why these properties are desirable.
本书共有 11 章,涵盖了类型编程的各个方面:
This book has 11 chapters covering various aspects of programming with types:
本书包含许多源代码示例,包括带编号的列表和与普通文本的内联。在这两种情况下,源代码都被格式化为 afixed-width font like this以将其与普通文本分开。有时,代码也会以粗体显示,以突出显示与本章之前的步骤相比发生变化的代码,例如当新功能添加到现有代码行时。
This book contains many examples of source code both in numbered listings and inline with normal text. In both cases, source code is formatted in a fixed-width font like this to separate it from ordinary text. Sometimes, code is also in bold to highlight code that has changed from previous steps in the chapter, such as when a new feature adds to an existing line of code.
在许多情况下,原始源代码已被重新格式化;我添加了换行符并修改了缩进以适应书中可用的页面空间。在极少数情况下,即使这样还不够,列表中包含行继续标记 ( )。此外,当在文本中描述代码时,源代码中的注释通常会从列表中删除。许多清单都附有代码注释,突出了重要的概念。
In many cases, the original source code has been reformatted; I’ve added line breaks and reworked indentation to accommodate the available page space in the book. In rare cases, even this was not enough, and listings include line-continuation markers (). Additionally, comments in the source code have often been removed from the listings when the code is described in the text. Code annotations accompany many of the listings, highlighting important concepts.
本书中的所有代码示例都可以在 GitHub 上找到,网址为https://github.com/vladris/programming-with-types/。该代码是使用 TypeScript 3.3 版构建的,针对 ES6 标准,具有严格的设置。
All the code samples in this book are available on GitHub at https://github.com/vladris/programming-with-types/. The code was built with version 3.3 of TypeScript, targeting the ES6 standard, with strict settings.
Vlad Riscutia 是 Microsoft 的一名软件工程师,拥有十多年的经验。在此期间,他领导了多个重大软件项目并指导了许多初级工程师。
Vlad Riscutia is a software engineer at Microsoft with more than a decade of experience. During this time, he has led several major software projects and mentored many junior engineers.
购买Programming with Types可以免费访问由 Manning Publications 运营的私人 Web 论坛,您可以在该论坛上对本书发表评论、提出技术问题,并获得作者和其他用户的帮助。要访问该论坛,请转至https://forums.manning.com/forums/programming-with-types。您还可以在https://forums.manning.com/forums/about上了解有关 Manning 论坛和行为规则的更多信息。
Purchase of Programming with Types includes free access to a private web forum run by Manning Publications where you can make comments about the book, ask technical questions, and receive help from the author and from other users. To access the forum, go to https://forums.manning.com/forums/programming-with-types. You can also learn more about Manning’s forums and the rules of conduct at https://forums.manning.com/forums/about.
Manning 对我们的读者的承诺是提供一个场所,让各个读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与任何特定数量的承诺,他们对论坛的贡献仍然是自愿的(并且是无偿的)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他的兴趣发生偏差!只要本书还在印刷,就可以从出版商的网站访问论坛和以前讨论的档案。
Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the author can take place. It is not a commitment to any specific amount of participation on the part of the author, whose contribution to the forum remains voluntary (and unpaid). We suggest you try asking the author some challenging questions lest his interest stray! The forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.
Programming with Types封面上的人物标题为“Fille Lipporette en habit de Noce”或“Liporette girl in wedding dress”。插图取自 Jacques Grasset de Saint-Sauveur(1757-1810 年)收集的来自不同国家的服饰,名为Costumes de Différents Pays,1797 年在法国出版。每幅插图均由手工精心绘制和着色。Grasset de Saint-Sauveur 丰富多样的藏品生动地提醒我们 200 年前世界城镇和地区在文化上的差异。人们彼此隔绝,说着不同的方言和语言。无论是在大街上还是在乡下,仅凭着装就很容易辨别出他们住在哪里,他们的行业或生活地位。
The figure on the cover of Programming with Types is captioned “Fille Lipporette en habit de Noce,” or “Liporette girl in wedding dress.” The illustration is taken from a collection of dress costumes from various countries by Jacques Grasset de Saint-Sauveur (1757–1810), titled Costumes de Différents Pays, published in France in 1797. Each illustration is finely drawn and colored by hand. The rich variety of Grasset de Saint-Sauveur’s collection reminds us vividly of how culturally apart the world’s towns and regions were just 200 years ago. Isolated from each other, people spoke different dialects and languages. In the streets or in the countryside, it was easy to identify where they lived and what their trade or station in life was just by their dress.
从那时起,我们的着装方式发生了变化,当时如此丰富的地区多样性已经消失。现在很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。也许我们已经用文化多样性换取了更加多样化的个人生活——当然是为了更加多样化和快节奏的技术生活。
The way we dress has changed since then and the diversity by region, so rich at the time, has faded away. It is now hard to tell apart the inhabitants of different continents, let alone different towns, regions, or countries. Perhaps we have traded cultural diversity for a more varied personal life—certainly for a more varied and fast-paced technological life.
在这个很难区分计算机书籍的时代,Manning 以两个世纪前区域生活的丰富多样性为基础,由 Grasset de Saint-索沃尔的照片。
At a time when it is hard to tell one computer book from another, Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional life of two centuries ago, brought back to life by Grasset de Saint-Sauveur’s pictures.
本章涵盖
This chapter covers
火星气候轨道飞行器在行星大气层中解体,因为洛克希德公司开发的一个部件产生了以磅力秒(美国单位)为单位的动量测量值,而美国宇航局开发的另一个部件预期以牛顿秒(公制单位)为单位测量动量。对这两种措施使用不同的类型可以避免灾难的发生。
The Mars Climate Orbiter disintegrated in the planet’s atmosphere because a component developed by Lockheed produced momentum measurements in pound-force seconds (U.S. units), whereas another component developed by NASA expected momentum to be measured in Newton seconds (metric units). Using different types for the two measures would have prevented the catastrophe.
正如我们将在本书中看到的那样,类型检查器提供了强大的方法来消除整类错误,前提是它们获得了足够的信息。随着软件复杂性的增加,提供更好的正确性保证的需求也随之增加。监控和测试可以显示软件在给定时间点、给定特定输入时是否按照规范运行。类型为我们提供了更通用的证据,证明无论输入如何,代码都将按照规范运行。
As we will see throughout this book, type checkers provide powerful ways to eliminate whole classes of errors, provided they are given enough information. As software complexity increases, so does the need to provide better correctness guarantees. Monitoring and testing can show that the software is behaving according to spec at a given point in time, given specific input. Types give us more general proofs that the code will behave according to spec regardless of input.
编程语言研究提出了越来越强大的类型系统。(例如,参见 Elm 和 Idris 等语言。)Haskell 越来越受欢迎。与此同时,正在努力将编译时类型检查引入动态类型语言:Python 添加了对类型提示的支持,而 TypeScript 是一种专门为 JavaScript 提供编译时类型检查而创建的语言。
Programming language research is coming up with ever-more-powerful type systems. (See, for example, languages like Elm and Idris.) Haskell is gaining in popularity. At the same time, there are ongoing efforts to bring compile-time type checking to dynamically typed languages: Python added support for type hints, and TypeScript is a language created for the sole purpose of providing compile-time type checking to JavaScript.
键入代码显然是有价值的,利用您的编程语言提供的类型系统的功能将帮助您编写更好、更安全的代码。
There clearly is value in typing code, and leveraging the features of the type systems that your programming languages provide will help you write better, safer code.
这是一本练习程序员的书。您应该能够熟练地使用 Java、C#、C++ 或 JavaScript/TypeScript 等主流编程语言编写代码。本书中的代码示例使用 TypeScript,但大部分内容与语言无关。事实上,示例并不总是使用惯用的 TypeScript。在可能的情况下,编写代码示例是为了让来自其他语言的程序员可以访问。请参阅附录 A 以了解如何构建本书中的代码示例,以及附录 B 以获取简短的 TypeScript 备忘单。
This is a book for practicing programmers. You should be comfortable writing code in a mainstream programming language like Java, C#, C++, or JavaScript/TypeScript. The code examples in this book are in TypeScript, but most of the content is language-agnostic. In fact, the examples don’t always use idiomatic TypeScript. Where possible, code examples are written to be accessible to programmers coming from other languages. See appendix A for how to build the code samples in this book and appendix B for a short TypeScript cheat sheet.
如果您在日常工作中开发面向对象的代码,您可能听说过代数数据类型 (ADT)、lambda、泛型、函子或 monad,并且想更好地了解它们是什么以及它们与您的代码有何关联工作。
If you are developing object-oriented code at your day job, you might have heard of algebraic data types (ADTs), lambdas, generics, functors, or monads, and would like to better understand what these are and how they are relevant to your work.
本书将教你如何依靠编程语言的类型系统来设计不易出错、组件化程度更高且更易于理解的代码。我们将看到可能在运行时发生并导致整个系统故障的错误如何转化为编译错误并在它们造成任何损害之前被捕获。
This book will teach you how to rely on the type system of your programming language to design code that is less error-prone, better componentized, and easier to understand. We’ll see how errors which could happen at run time and cause an entire system to malfunction can be transformed into compilation errors and caught before they can cause any damage.
许多关于类型系统的文献都是正式的。本书侧重于类型系统的实际应用;因此,数学被保持在最低限度。也就是说,您应该熟悉函数和集合等基本代数概念。我们将依靠这些来解释一些相关的概念。
A lot of the literature on type systems is formal. This book focuses on practical applications of type systems; thus, math is kept to a minimum. That being said, you should be familiar with basic algebra concepts like functions and sets. We will rely on these to explain some of the relevant concepts.
在硬件和机器代码的底层,程序逻辑(代码)及其操作的数据都表示为位。在此级别,代码和数据之间没有区别,因此当系统将一个误认为另一个时,很容易发生错误。这些错误的范围从程序崩溃到严重的安全漏洞,在这些漏洞中,攻击者“欺骗”系统将其输入数据作为代码执行。
At the low level of hardware and machine code, the program logic (the code) and the data it operates on are both represented as bits. At this level, there is no difference between the code and the data, so errors can easily happen when the system mistakes one for the other. These errors range from program crashes to severe security vulnerabilities in which an attacker “tricks” the system into executing their input data as code.
这种松散解释的一个例子是 JavaScripteval()函数,它将字符串计算为代码。当提供的字符串是有效的 Java-Script 代码时,它运行良好,但如果不是,则会导致运行时错误,如下一个清单所示。
An example of this kind of loose interpretation is the JavaScript eval() function, which evaluates a string as code. It works well when the string provided is valid Java-Script code but causes a run-time error when it isn’t, as shown in the next listing.
console.log(eval("40+2")); 1个
console.log(eval("Hello world!")); 2个console.log(eval("40+2")); 1
console.log(eval("Hello world!")); 2
除了区分代码和数据之外,我们还需要知道如何解释一段数据。16 位序列1100001010100011可以表示无符号 16 位整数49827、有符号 16 位整数-15709、UTF-8 编码字符或完全不同的东西,如图1.1'£'所示。我们的程序运行的硬件将所有内容存储为位序列,因此我们需要一个额外的层来赋予这些数据意义。
Beyond distinguishing between code and data, we need to know how to interpret a piece of data. The 16-bit sequence 1100001010100011 can represent the unsigned 16-bit integer 49827, the signed 16-bit integer -15709, the UTF-8 encoded character '£', or something completely different, as we can see in figure 1.1. The hardware our programs run on stores everything as sequences of bits, so we need an extra layer to give meaning to this data.
类型为这些数据赋予意义,并告诉我们的软件如何在给定的上下文中解释给定的位序列,以便它保留预期的含义。
Types give meaning to this data and tell our software how to interpret a given sequence of bits in a given context so that it preserves the intended meaning.
类型还限制了变量可以采用的有效值集。带符号的 16 位整数可以表示从-32768到的任何整数值32767,但不能表示其他任何整数。限制允许值范围的能力有助于通过不允许无效值在运行时出现来消除整个错误类别,如图1.2所示。将类型视为可能值的集合对于理解本书涵盖的许多概念很重要。
Types also constrain the set of valid values a variable can take. A signed 16-bit integer can represent any integer value from -32768 to 32767 but nothing else. The ability to restrict the range of allowed values helps eliminate whole classes of errors by not allowing invalid values to appear at run time, as shown in figure 1.2. Viewing types as sets of possible values is important to understanding many of the concepts covered in this book.
正如我们将在 1.3 节中看到的,当我们向代码添加属性时,系统会强制执行许多其他安全属性,例如将值标记为const或将成员标记为private。
As we will see in section 1.3, many other safety properties are enforced by the system when we add properties to our code, such as marking a value as const or a member as private.
因为本书讨论的是类型和类型系统,所以让我们在继续之前定义这些术语。
Because this book talks about types and type systems, let’s define these terms before moving forward.
类型是一种数据分类,它定义了可以对该数据执行的操作、数据的含义以及允许值的集合。编译器和/或运行时会检查类型,以确保数据的完整性、实施访问限制并按开发人员的意图解释数据。
A type is a classification of data that defines the operations that can be done on that data, the meaning of the data, and the set of allowed values. Typing is checked by the compiler and/or run time to ensure the integrity of the data, enforce access restrictions, and interpret the data as meant by the developer.
在某些情况下,我们将简化我们的讨论并忽略操作部分,因此我们将类型简单地视为集合,它表示该类型的实例可以采用的所有可能值。
In some cases, we will simplify our discussion and ignore the operations part, so we’ll look at types simply as sets, which represent all the possible values an instance of that type can take.
类型系统是一组规则,用于为编程语言的元素分配和强制类型。这些元素可以是变量、函数和其他更高级别的结构。类型系统通过您在代码中提供的符号或通过基于上下文推断特定元素的类型来隐式分配类型。它们允许类型之间的各种转换,而不允许其他类型。
A type system is a set of rules that assigns and enforces types to elements of a programming language. These elements can be variables, functions, and other higher-level constructs. Type systems assign types through notation you provide in the code or implicitly by deducing the type of a certain element based on context. They allow various conversions between types and disallow others.
现在我们已经定义了类型和类型系统,让我们看看如何执行类型系统的规则。图 1.3在较高层次上显示了源代码是如何执行的。
Now that we’ve defined types and type systems, let’s see how the rules of a type system are enforced. Figure 1.3 shows, at a high-level, how source code gets executed.
在非常高的层次上,我们编写的源代码被编译器或解释器转换为机器指令或运行时指令。这个运行时可以是物理计算机,在这种情况下指令是 CPU 指令,也可以是虚拟机,有自己的指令集和设施。
At a very high level, the source code we write gets transformed by a compiler or interpreter into instructions for a machine, or run time. This run time can be a physical computer, in which case the instructions are CPU instructions, or it can be a virtual machine, with its own instruction set and facilities.
类型检查过程确保程序遵守类型系统的规则。这种类型检查由编译器在转换代码时完成,或由运行时在执行代码时完成。处理类型规则执行的编译器组件称为类型检查器。
The process of type checking ensures that the rules of the type system are respected by the program. This type checking is done by the compiler when converting the code or by the run time while executing the code. The component of the compiler that handles enforcement of the typing rules is called a type checker.
如果类型检查失败,意味着程序不遵守类型系统的规则,我们将以编译失败或运行时错误告终。我们将在 1.4 节中更详细地讨论编译时类型检查与执行时(或运行时)类型检查之间的区别。
If type checking fails, meaning that the rules of the type system are not respected by the program, we end up with a failure to compile or with a run-time error. We will go over the difference between compile-time type checking versus execution-time (or run-time) type checking in more detail in section 1.4.
类型系统背后有很多正式的理论。值得注意的 Curry-Howard 对应关系,也称为程序证明,显示了逻辑与类型理论之间的密切联系。它表明我们可以将类型视为逻辑命题,将一种类型到另一种类型的函数视为逻辑蕴涵。类型的值等同于命题为真的证据。
There is a lot of formal theory behind type systems. The remarkable Curry-Howard correspondence, also known as proofs-as-programs, shows the close connection between logic and type theory. It shows that we can view a type as a logic proposition, and a function from one type to another as a logic implication. A value of a type is equivalent to evidence that the proposition is true.
获取一个函数,该函数接收 a 作为参数boolean并返回 a string。
Take a function that receives as argument a boolean and returns a string.
布尔值到字符串
Boolean to string
函数 booleanToString(b: 布尔值): 字符串 {
如果(二){
返回“真”;
} 别的 {
返回“假”;
}
}function booleanToString(b: boolean): string {
if (b) {
return "true";
} else {
return "false";
}
}
这个功能也可以理解为“boolean暗示string”。给定命题的证据boolean,这个函数(蕴涵)可以产生命题的证据string。的证据boolean是该类型的值,true或者false。当我们有了它时,这个函数(蕴含)将为我们提供 as 的证据stringas 要么 string"true"要么 string "false"。
This function can also be interpreted as “boolean implies string.” Given evidence of the proposition boolean, this function (implication) can produce evidence of the proposition string. Evidence of boolean is a value of that type, true or false. When we have that, this function (implication) will give us evidence of string as either the string "true" or the string "false".
逻辑与类型理论之间的密切关系表明,尊重类型系统规则的程序等同于逻辑证明。换句话说,类型系统是我们用来编写这些证明的语言。Curry-Howard 对应关系很重要,因为它为保证程序正确运行提供了逻辑严谨性。
The close relationship between logic and type theory shows that a program that respects the type system rules is equivalent to a logic proof. In other words, the type system is the language in which we write these proofs. The Curry-Howard correspondence is important because it brings logic rigor to the guarantees that a program will behave correctly.
因为最终数据都是 0 和 1,所以数据的属性,例如如何解释它、它是否不可变以及它的可见性,都是类型级别的属性。我们将变量声明为数字,类型检查器确保我们不会将其数据解释为字符串。我们将变量声明为私有或只读,虽然数据本身在内存与公共可变数据没有什么不同,类型检查器可以确保我们不会在其范围之外引用私有变量或尝试更改只读数据。
Because ultimately data is all 0s and 1s, properties of the data, such as how to interpret it, whether it is immutable, and its visibility, are type-level properties. We declare a variable as a number, and the type checker ensures that we don’t interpret its data as a string. We declare a variable as private or read-only, and although the data itself in memory is no different from public mutable data, the type checker can make sure we do not refer to a private variable outside its scope or try to change read-only data.
类型化的主要好处是正确性、不变性、封装性、可组合性和可读性。这五个都是良好软件设计和行为的基本特征。系统随着时间的推移而发展。这些特征抵消了不可避免地试图潜入系统的熵。
The main benefits of typing are correctness, immutability, encapsulation, composability, and readability. All five are fundamental features of good software design and behavior. Systems evolve over time. These features counterbalance the entropy that inevitably tries to creep into the system.
正确的代码意味着代码根据其规范运行,产生预期的结果而不会产生运行时错误或崩溃。类型帮助我们对代码增加更多的严格性,以确保它的行为正确。
Correct code means code that behaves according to its specification, producing expected results without creating run-time errors or crashes. Types help us add more strictness to the code to ensure that it behaves correctly.
例如,假设我们想"Script"在另一个字符串中找到该字符串的索引。在不提供足够的类型信息的情况下,我们可以允许类型的值any作为参数传递给我们的函数。如果参数不是字符串,我们将遇到运行时错误,如下一个清单所示。
As an example, let’s say we want to find the index of the string "Script" within another string. Without providing enough type information, we can allow a value of any type to be passed as an argument to our function. We are going to hit run-time errors if the argument is not a string, as the next listing shows.
函数 scriptAt(s: any): number { 1
返回 s.indexOf("脚本");
}
console.log(scriptAt("TypeScript")); 2
console.log(scriptAt(42)); 3个function scriptAt(s: any): number { 1
return s.indexOf("Script");
}
console.log(scriptAt("TypeScript")); 2
console.log(scriptAt(42)); 3
该程序不正确,因为它42不是scriptAt函数的有效参数,但编译器没有拒绝它,因为我们没有提供足够的类型信息。string让我们通过将参数限制为下一个清单中 类型的值来优化代码。
The program is incorrect, as 42 is not a valid argument to the scriptAt function, but the compiler did not reject it because we hadn’t provided enough type information. Let’s refine the code by constraining the argument to a value of type string in the next listing.
函数 scriptAt(s: string ): number { 1
返回 s.indexOf("脚本");
}
console.log(scriptAt("TypeScript"));
console.log(scriptAt(42)); 2个function scriptAt(s: string): number { 1
return s.indexOf("Script");
}
console.log(scriptAt("TypeScript"));
console.log(scriptAt(42)); 2
现在错误的程序被编译器拒绝并显示以下错误消息:
Now the incorrect program is rejected by the compiler with this error message:
“42”类型的参数不可分配给“字符串”类型的参数
Argument of type '42' is not assignable to parameter of type 'string'
利用类型系统,我们将过去可能在生产中遇到并影响我们客户的运行时问题转变为我们必须在部署代码之前解决的无害编译时问题。类型检查器确保我们永远不会尝试将苹果作为橙子传递;因此,我们的代码变得更加健壮。
Leveraging the type system, we transformed what used to be a run-time issue that could have been hit in production, affecting our customers, into a harmless compile-time issue that we must fix before deploying our code. The type checker makes sure we never try to pass apples as oranges; thus, our code becomes more robust.
当程序进入错误状态时会发生错误,这意味着无论出于何种原因,其所有活动变量的当前组合都是无效的。消除其中一些不良状态的一种技术是通过限制变量可能取值的数量来减少状态空间,如图1.4所示。
Errors occur when a program gets into a bad state, which means that the current combination of all its live variables is invalid for whatever reason. One technique for eliminating some of these bad states is reducing the state space by constraining the number of possible values that variables can take, like in figure 1.4.
我们可以将正在运行的程序的状态空间定义为其所有活动变量的所有可能值的组合。即每个变量类型的笛卡尔积。请记住,类型可以被视为变量的一组可能值。两个集合的笛卡尔积是由两个集合中的所有有序对组成的集合。
We can define the state space of a running program as the combination of all possible values of all its live variables. That is, the Cartesian product of the type of each variable. Remember, a type can be viewed as a set of possible values for a variable. The Cartesian product of two sets is the set comprised of all ordered pairs from the two sets.
不允许潜在不良状态的一个重要副产品是更安全的代码。许多攻击依赖于执行用户提供的数据、缓冲区溢出和其他此类技术,通常可以通过足够强大的类型系统和良好的类型定义来缓解这些攻击。
An important byproduct of disallowing potential bad states is more secure code. Many attacks rely on executing user-provided data, buffer overruns, and other such techniques, which can often be mitigated with a strong-enough type system and good type definitions.
代码正确性不仅仅是消除代码中的无辜错误,还包括防止恶意攻击。
Code correctness goes beyond eliminating innocent bugs in the code to preventing malicious attacks.
不变性是另一个与将我们的运行系统视为在其状态空间中移动密切相关的属性。当我们处于已知良好的状态时,如果我们能够保持该状态的某些部分不发生变化,我们就可以减少出错的可能性。
Immutability is another property closely related to viewing our running system as moving through its state space. When we are in a known-good state, if we can keep parts of that state from changing, we reduce the possibility of errors.
让我们举一个简单的例子,我们试图通过0检查除数的值来防止除法,如果除数是 则抛出错误0,如以下清单所示。如果我们检查后值可以改变,则支票不是很有价值。
Let’s take a simple example in which we attempt to prevent division by 0 by checking the value of our divisor and throwing an error if the divisor is 0, as shown in the following listing. If the value can change after we inspect it, the check is not very valuable.
函数 safeDivide(): 数字 {
让 x: 数字 = 42;
if (x == 0) throw new Error("x should not be 0"); 1个
x = x - 42; 2个
返回 42/x; 3
}function safeDivide(): number {
let x: number = 42;
if (x == 0) throw new Error("x should not be 0"); 1
x = x - 42; 2
return 42 / x; 3
}
这种情况在实际程序中一直以微妙的方式发生:一个变量被不同的线程同时更改,或者被另一个调用的函数模糊地更改。就像在这个例子中一样,一旦一个值发生变化,我们就会失去我们希望从我们执行的检查中获得的任何保证。作为x一个常量,当我们试图在下一个清单中改变它时,我们会得到一个编译错误。
This happens all the time in real programs, in subtle ways: a variable gets changed concurrently by a different thread or obscurely by another called function. Just as in this example, as soon as a value changes, we lose any guarantees we were hoping to get from the checks we performed. Making x a constant, we get a compilation error when we try to mutate it in the next listing.
function safeDivide(): number {
const x: number = 42; 1个
if (x == 0) throw new Error("x should not be 0");
x = x - 42; 2个
返回 42/x;
}function safeDivide(): number {
const x: number = 42; 1
if (x == 0) throw new Error("x should not be 0");
x = x - 42; 2
return 42 / x;
}
该错误被编译器拒绝并显示以下错误消息:
The bug is rejected by the compiler with the following error message:
无法分配给“x”,因为它是常量。
Cannot assign to 'x' because it is a constant.
就内存表示而言,可变和不可变之间没有区别x。constness 属性仅对编译器有意义。它是由类型系统启用的属性。
In terms of in-memory representation, there is no difference between a mutable and an immutable x. The constness property is meaningful only for the compiler. It is a property enabled by the type system.
通过将符号添加到我们的类型来标记不应该改变的状态const可以防止我们失去之前保证的那种突变检查。当涉及并发时,不变性特别有用,因为如果数据不可变,数据竞争就变得不可能。
Marking state that shouldn’t change as such by adding the const notation to our type prevents the kind of mutations with which we lose guarantees we previously checked for. Immutability is especially useful when concurrency is involved, as data races become impossible if data is immutable.
优化编译器在处理不可变变量时可以发出更高效的代码,因为它们的值可以内联。一些函数式编程语言使所有数据不可变:一个函数将一些数据作为输入并返回其他数据而不改变其输入。在这种情况下,当我们验证一个变量并确认它处于良好状态时,我们可以保证它在整个生命周期内都处于良好状态。当然,代价是当我们可以就地操作数据时,我们最终会复制数据,这并不总是可取的。
Optimizing compilers can emit more-efficient code when dealing with immutable variables, as their values can be inlined. Some functional programming languages make all data immutable: a function takes some data as input and returns other data without ever changing its input. In such cases, when we validate a variable and confirm that it is in a good state, we are guaranteed it will be in a good state for its whole lifetime. The trade-off, of course, is that we end up copying data when we could have operated on it in-place, which is not always desirable.
使一切不可变可能并不总是可行的。话虽这么说,尽可能多地使数据不可变将极大地减少诸如不满足先决条件和数据竞争等问题的机会。
Making everything immutable might not always be feasible. That being said, making as much of the data immutable as you reasonably can will tremendously reduce the opportunity for issues such as preconditions not being met and data races.
封装是隐藏我们代码的某些内部结构的能力,无论是函数、类还是模块。您可能知道,封装是可取的,因为它可以帮助我们处理复杂性:我们将代码拆分为更小的组件,每个组件仅向外界公开严格需要的内容,而其实现细节则保持隐藏和隔离。
Encapsulation is the ability to hide some of the internals of our code, be it a function, a class, or a module. As you probably know, encapsulation is desirable, as it helps us deal with complexity: we split the code into smaller components, and each component exposes only what is strictly needed to the outside world, while its implementation details are kept hidden and isolated.
在下一个清单中,让我们将安全除法示例扩展到一个试图确保除法永远0不会发生的类。
In the next listing, let’s extend our safe division example to a class that tries to ensure that division by 0 never happens.
类 SafeDivisor {
除数:数字= 1;
设置除数(值:数字){
if (value == 0) throw new Error("Value should not be 0"); 1个
this.divisor = 值;
}
除法(x:数字):数字{
返回 x / this.divisor; 2个
}
}
函数利用():数字{
让 sd = new SafeDivisor();
sd.divisor = 0; 3
返回 sd.divide(42); 4
}class SafeDivisor {
divisor: number = 1;
setDivisor(value: number) {
if (value == 0) throw new Error("Value should not be 0"); 1
this.divisor = value;
}
divide(x: number): number {
return x / this.divisor; 2
}
}
function exploit(): number {
let sd = new SafeDivisor();
sd.divisor = 0; 3
return sd.divide(42); 4
}
在这种情况下,我们不能再使除数不可变,因为我们确实希望 API 的调用者能够更新它。问题是调用者可以绕过0检查并直接设置divisor为任何值,因为它对他们可见。这种情况下的解决方法是将其标记为private并将其范围限定为类,如以下清单所示。
In this case we can no longer make the divisor immutable, as we do want to give callers of our API the ability to update it. The problem is that callers can bypass the 0 check and directly set divisor to any value because it is visible to them. The fix in this case is to mark it as private and scope it to the class, as the following listing shows.
class SafeDivisor {
私有除数:number = 1; 1个
设置除数(值:数字){
if (value == 0) throw new Error("Value should not be 0");
this.divisor = 值;
}
除法(x:数字):数字{
返回 x / this.divisor;
}
}
函数利用(){
让 sd = new SafeDivisor();
sd.divisor = 0; 2个
sd.divide(42);
}class SafeDivisor {
private divisor: number = 1; 1
setDivisor(value: number) {
if (value == 0) throw new Error("Value should not be 0");
this.divisor = value;
}
divide(x: number): number {
return x / this.divisor;
}
}
function exploit() {
let sd = new SafeDivisor();
sd.divisor = 0; 2
sd.divide(42);
}
Apublic和private成员具有相同的内存表示;有问题的代码在第二个示例中不再编译的事实仅仅是由于我们提供的类型符号。事实上,public、private和其他可见性类型是它们出现的类型的属性。
A public and a private member have the same in-memory representation; the fact that the problematic code no longer compiles in the second example is simply due to the type notations we provided. In fact, public, private, and other visibility kinds are properties of the type in which they appear.
封装或信息隐藏使我们能够跨公共接口和非公共实现拆分逻辑和数据。这在大型系统中非常有用,因为针对接口(或抽象)工作可以减少理解一段特定代码的作用所需的脑力劳动。我们只需要了解和推理组件的接口,而不是它们的所有实现细节。它还通过在边界内确定非公开信息的范围来提供帮助,并保证外部代码无法修改它,因为它根本无法访问它。
Encapsulation, or information hiding, enables us to split logic and data across a public interface and a nonpublic implementation. This is extremely helpful in large systems, as working against interfaces (or abstractions) reduces the mental effort it takes to understand what a particular piece of code does. We need to understand and reason about only the interfaces of components, not all their implementation details. It also helps by scoping nonpublic information within a boundary and guarantees that external code cannot modify it, as it simply does not have access to it.
封装出现在多个层次:服务将其 API 作为接口公开,模块导出其接口并隐藏实现细节,类仅公开其公共成员,等等。就像嵌套娃娃一样,代码的两部分之间的关系越弱,它们共享的信息就越少。这加强了组件可以对其内部管理的数据做出的保证,因为不允许外部代码在不通过组件接口的情况下修改它。
Encapsulation appears at multiple layers: a service exposes its API as an interface, a module exports its interface and hides implementation details, a class exposes only its public members, and so on. Like nesting dolls, the weaker the relationship between two parts of the code, the less information they share. This strengthens the guarantees a component can make about the data it manages internally, as no outside code can be allowed to modify it without going through the component’s interface.
假设我们要在数字数组中找到第一个负数,并在字符串数组中找到第一个单字符字符串。如果不考虑如何将这个问题分解成可组合的部分并将它们重新组合成一个可组合的系统,我们最终可能会得到两个函数:findFirstNegativeNumber()和findFirstOneCharacterString(),如以下清单所示。
Let’s say we want to find the first negative number in an array of numbers and the first one-character string in an array of strings. Without thinking about how we can break down this problem into composable pieces and put them back together into a composable system, we could end up with two functions: findFirstNegativeNumber() and findFirstOneCharacterString(), as shown in the following listing.
函数 findFirstNegativeNumber(numbers: number[])
: 号码 | 不明确的 {
对于(让我的数字){
如果 (i < 0) 返回 i;
}
}
函数 findFirstOneCharacterString(字符串:字符串 [])
: 串 | 不明确的 {
for (let str of strings) {
如果 (str.length == 1) 返回 str;
}
}function findFirstNegativeNumber(numbers: number[])
: number | undefined {
for (let i of numbers) {
if (i < 0) return i;
}
}
function findFirstOneCharacterString(strings: string[])
: string | undefined {
for (let str of strings) {
if (str.length == 1) return str;
}
}
这两个函数分别搜索第一个负数和第一个单字符字符串。如果没有找到这样的元素,则函数返回undefined(隐含地,通过在没有语句的情况下退出函数return)。
The two functions search for the first negative number and for the first one-character string, respectively. If no such element is found, the functions return undefined (implicitly, by exiting the function without a return statement).
如果出现新要求,我们也应该在找不到元素时记录错误,我们需要更新这两个函数,如下一个清单所示。
If a new requirement comes in that we should also log an error whenever we fail to find an element, we need to update both functions, as shown in the next listing.
函数 findFirstNegativeNumber(numbers: number[])
: 号码 | 不明确的 {
对于(让我的数字){
如果 (i < 0) 返回 i;
}
console.error("没有找到匹配的值");
}
函数 findFirstOneCharacterString(字符串:字符串 [])
: 串 | 不明确的 {
for (let str of strings) {
如果 (str.length == 1) 返回 str;
}
console.error("没有找到匹配的值");
}function findFirstNegativeNumber(numbers: number[])
: number | undefined {
for (let i of numbers) {
if (i < 0) return i;
}
console.error("No matching value found");
}
function findFirstOneCharacterString(strings: string[])
: string | undefined {
for (let str of strings) {
if (str.length == 1) return str;
}
console.error("No matching value found");
}
这已经不太理想了。如果我们忘记在所有地方应用更新怎么办?这些问题在大型系统中更加复杂。仔细观察每个函数的作用,我们可以看出算法是相同的;但在一种情况下,我们在一个条件下对数字进行操作,而在另一种情况下,我们在不同条件下对字符串进行操作。我们可以提供一个通用算法,该算法根据其操作的类型和检查的条件进行参数化,如以下清单所示。这样的算法不依赖于系统的其他部分,我们可以孤立地对其进行推理。
This is already less than ideal. What if we forget to apply the update everywhere? Such issues compound in large systems. Looking more closely at what each function does, we can tell that the algorithm is the same; but in one case, we operate on numbers with one condition, and in the other, we operate on strings with a different condition. We can provide a generic algorithm parameterized on the type it operates on and the condition it checks for, as shown in the following listing. Such an algorithm does not depend on the other parts of the system, and we can reason about it in isolation.
first<T>(range: T[], p: (elem: T) => 布尔值)
: 吨 | 不明确的 {
for (let elem of range) {
如果(p(元素))返回元素;
}
}
函数 findFirstNegativeNumber(numbers: number[])
: 号码 | 不明确的 {
先返回(数字,n => n < 0);
}
函数 findFirstOneCharacterString(字符串:字符串 [])
: 串 | 不明确的 {
首先返回(字符串,str => str.length == 1);
}function first<T>(range: T[], p: (elem: T) => boolean)
: T | undefined {
for (let elem of range) {
if (p (elem)) return elem;
}
}
function findFirstNegativeNumber(numbers: number[])
: number | undefined {
return first(numbers, n => n < 0);
}
function findFirstOneCharacterString(strings: string[])
: string | undefined {
return first(strings, str => str.length == 1);
}
如果这个语法看起来有点奇怪,请不要担心;我们将n => n < 0在第 5 章介绍内联函数,在第 9章和第 10章介绍泛型。
Don’t worry if the syntax of this looks a bit strange; we’ll cover inline functions such as n => n < 0 in chapter 5 and generics in chapters 9 and 10.
如果我们想向这个实现中添加日志记录,我们只需要更新first. 更好的是,如果我们想出一个更有效的算法,只需更新实现就能使所有调用者受益。
If we want to add logging to this implementation, we need only to update the implementation of first. Better still, if we figure out a more efficient algorithm, simply updating the implementation benefits all callers.
正如我们将在第 10 章讨论泛型算法和迭代器时了解到的那样,我们可以使这个函数更加通用。目前,它只对某种类型的数组进行操作T。它可以扩展到遍历任何数据结构。
As we’ll learn in chapter 10 when we discuss generic algorithms and iterators, we can make this function even more general. Currently, it only operates on an array of some type T. It can be extended to traverse any data structure.
如果代码不可组合,我们需要为每种数据类型、数据结构和条件提供不同的功能,即使它们从根本上实现了相同的抽象。具有抽象然后混合和匹配组件的能力减少了很多重复。通用类型使我们能够表达这些类型的抽象。
If the code is not composable, we need a different function for each data type, data structure, and condition, even though they all fundamentally implement the same abstraction. Having the ability to abstract and then mix and match components reduces a lot of duplication. Generic types enable us to express these kinds of abstractions.
具有组合独立组件的能力可以产生模块化系统和更少的代码来维护。随着代码大小和组件数量的增加,可组合性变得很重要。在可组合系统中,各个部分是松散耦合的;同时,代码不会在每个子系统中重复。通常可以通过更新单个组件而不是在整个系统中进行大量更改来合并新需求,同时了解这样的系统需要更少的思考,因为我们可以孤立地对其部分进行推理。
Having the ability to combine independent components yields a modular system and less code to maintain. Composability becomes important as the size of the code and the number of components increase. In a composable system, the parts are loosely coupled; at the same time, code does not get duplicated in each subsystem. New requirements can usually be incorporated by updating a single component instead of making large changes across the whole system, at the same time understanding that such a system requires less thought, as we can reason about its parts in isolation.
代码被阅读的次数比编写的次数多得多。类型化清楚地表明了函数对其参数的期望、通用算法的先决条件是什么、类实现了哪些接口等等。这些信息很有价值,因为我们可以单独推理可读代码:仅通过查看定义,我们应该能够轻松理解代码应该如何工作,而无需浏览源代码来查找调用者和被调用者。
Code is read many more times than it is written. Typing makes it clear what a function expects from its arguments, what the prerequisites for a generic algorithm are, what interfaces a class implements, and so on. This information is valuable because we can reason about readable code in isolation: just by looking at a definition, we should be able to easily understand how the code is supposed to work without having to navigate the sources to find callers and callees.
命名和注释也是其中的重要部分,但是键入会增加另一层信息,因为它允许我们命名约束。让我们看看find()下面清单中的无类型函数声明。
Naming and comments are important parts of this, too, but typing adds another layer of information, as it allows us to name constraints. Let’s look at an untyped find() function declaration in the following listing.
声明函数 find(range: any, pred: any): any;
declare function find(range: any, pred: any): any;
只看这个函数,很难说出它需要什么样的参数。我们需要阅读实现,传递我们最好的猜测,看看我们是否遇到运行时错误,或者希望文档涵盖这一点。
Just looking at this function, it’s hard to tell what kind of arguments it expects. We need to read the implementation, pass in our best guess, and see whether we get a run-time error or hope that the documentation covers this.
将以下代码与前面的声明进行对比。
Contrast the following code with the previous declaration.
先声明函数<T>(range: T[],
p: (elem: T) => 布尔值: T | 不明确的;declare function first<T>(range: T[],
p: (elem: T) => boolean): T | undefined;
阅读这个声明,我们看到对于任何类型T,我们需要提供一个数组T[]作为range参数和一个接受 aT并返回 aboolean作为 -p参数的函数。我们还可以立即看到该函数将返回Tor - undefined。
Reading this declaration, we see that for any type T, we need to provide an array T[] as the range argument and a function that takes a T and returns a boolean as the -p argument. We can also immediately see that the function is going to return a T or -undefined.
无需查找实现或查找文档,只需阅读此声明即可准确告诉我们要传递的参数类型并减少我们的认知负担,因为我们可以将其视为一个独立的独立实体。明确显示此类类型信息,不仅可供编译器使用,也可供开发人员使用,这使得理解代码变得容易得多。
Instead of having to find the implementation or look up the documentation, just reading this declaration tells us exactly what type of arguments to pass and reduces our cognitive load, as we can treat it as a self-contained, separate entity. Having such type information explicit, available not only to the compiler but also to the developer, makes understanding the code a lot easier.
大多数现代语言都提供某种程度的类型推断,这意味着根据上下文推断变量的类型。这很有用,因为它可以节省我们的冗余输入,但是当编译器可以轻松理解代码而人们这样做太费力时,就会成为一个问题。拼写出来的类型比注释更有价值,因为它是由编译器强制执行的。
Most modern languages provide some level of type inference, which means deducing the type of a variable based on context. This is useful, as it saves us redundant typing, but becomes a problem when the compiler can understand the code easily while it becomes too effortful for people to do so. A spelled-out type is much more valuable than a comment, as it is enforced by the compiler.
如今,大多数语言和运行时都提供某种形式的类型。我们很久以前就意识到,能够将代码解释为数据,将数据解释为代码可能会导致灾难性的后果。结果。现代类型系统之间的主要区别在于何时检查类型以及检查的严格程度。
Nowadays, most languages and run times provide some form of typing. We realized long ago that being able to interpret code as data and data as code can lead to catastrophic results. The main distinction between contemporary type systems lies in when types get checked and how strict the checks are.
对于静态类型,类型检查是在编译时执行的,因此当编译完成时,运行时值可以保证具有正确的类型。另一方面,动态类型将类型检查推迟到运行时,因此类型不匹配会变成运行时错误。
With static typing, type checking is performed at compile time, so when compilation is done, the run-time values are guaranteed to have correct types. Dynamic typing, on the other hand, defers type checking to the run time, so type mismatches become run-time errors.
强类型几乎不进行任何隐式类型转换,而较弱的类型系统允许进行更多的隐式类型转换。
Strong typing does few if any implicit type conversions, whereas weaker type systems allow more implicit type conversions.
JavaScript 是动态类型的,而 TypeScript 是静态类型的。事实上,创建 TypeScript 是为了向 JavaScript 添加静态类型检查。将运行时错误转化为编译错误,尤其是在大型应用程序中,可以使代码更易于维护和恢复。本书侧重于静态类型和静态类型语言,但理解替代方案是很好的。
JavaScript is dynamically typed, and TypeScript is statically typed. In fact, TypeScript was created to add static type checking to JavaScript. Converting what would otherwise be run-time errors to compilation errors, especially in large applications, makes code more maintainable and resilient. This book focuses on static typing and statically typed languages, but it’s good to understand the alternative.
动态类型不会在编译时强加任何类型约束。俗称鸭子类型来自于“如果它像鸭子一样蹒跚而行并且像鸭子一样嘎嘎叫,那么它一定是鸭子”。代码可以尝试以任何它想要的方式自由使用变量,并且键入由运行时应用。我们可以使用any关键字在 TypeScript 中模拟动态类型,它允许无类型变量。
Dynamic typing does not impose any typing constraints at compile time. The colloquial name duck typing comes from the phrase “If it waddles like a duck and it quacks like a duck, it must be a duck.” Code can attempt to freely use a variable in any way it wants, and typing is applied by the run time. We can simulate dynamic typing in TypeScript by using the any keyword, which allows untyped variables.
我们可以实现一个quacker()函数,它接受一个duck类型的参数any并调用quack()它。只要我们向它传递一个具有quack()方法的对象,一切正常。另一方面,如果我们传递一些不能传递的东西quack(),我们会得到一个运行时TypeError,如以下清单所示。
We can implement a quacker() function that takes a duck argument of type any and calls quack() on it. As long as we pass it an object that has a quack() method, everything works. If, on the other hand, we pass something that can’t quack(), we get a run-time TypeError, as shown in the following listing.
功能嘎嘎(鸭子:任何){ 1
鸭子嘎嘎();
}
嘎嘎({ 嘎嘎: function () { console.log("嘎嘎"); } }); 2
嘎嘎(42); 3个function quacker(duck: any) { 1
duck.quack();
}
quacker({ quack: function () { console.log("quack"); } }); 2
quacker(42); 3
另一方面,静态类型在编译时执行类型检查,因此尝试传递错误类型的参数会导致编译错误。为了利用 TypeScript 的静态类型特性,我们可以通过声明一个接口并正确输入函数的参数来更新代码,如清单 1.14Duck所示。请注意,在 TypeScript 中,我们不必显式声明我们正在实现接口。只要我们提供一个函数,编译器就会认为Duckquack()要实现的接口。在其他语言中,我们必须通过将类声明为实现接口来显式声明。
Static typing, on the other hand, performs type checks at compile time, so attempting to pass an argument of the wrong type causes a compilation error. To leverage the static typing features of TypeScript, we can update the code by declaring a Duck interface and properly typing the function’s argument, as shown in listing 1.14. Note that in TypeScript, we do not have to explicitly declare that we are implementing the Duck interface. As long as we provide a quack() function, the compiler considers the interface to be implemented. In other languages, we would have to be explicit by declaring a class as implementing the interface.
接口鸭{ 1
嘎嘎():无效;
}
功能嘎嘎(鸭子:鸭子){ 2
鸭子嘎嘎();
}
嘎嘎({ 嘎嘎: function () { console.log("嘎嘎"); } });
庸医(42); 3个interface Duck { 1
quack(): void;
}
function quacker(duck: Duck) { 2
duck.quack();
}
quacker({ quack: function () { console.log("quack"); } });
quacker(42); 3
在编译时捕获这些类型的错误,在它们导致正在运行的程序出现故障之前,是静态类型的主要好处。
Catching these types of errors at compile time, before they can cause a running program to malfunction, is the key benefit of static typing.
我们经常听到用强类型和弱类型来描述类型系统。类型系统的强度描述了系统在执行类型约束方面的严格程度。弱类型系统隐式尝试将值从其实际类型转换为使用该值时预期的类型。
We often hear the terms strong typing and weak typing to describe a type system. The strength of a type system describes how strict the system is with regard to enforcing type constraints. A weak type system implicitly tries to convert values from their actual types to the types expected when the value is used.
考虑这个问题:牛奶是否等于白色?在强类型世界中,不,牛奶是液体,将它与颜色进行比较是没有意义的。在弱类型的世界中,我们可以说,“嗯,牛奶的颜色是白色的,所以是的,它确实等于白色。” 在强类型世界中,我们可以通过使问题更明确来明确地将牛奶转换为颜色:牛奶的颜色是否等于白色?在弱类型的世界中,我们不需要这种细化。
Consider this question: Does milk equal white? In a strongly typed world, no, milk is a liquid, and it makes no sense to compare it to a color. In a weakly typed world, we can say, “Well, milk’s color is white, so yes, it does equal white.” In the strongly typed world, we can explicitly convert milk to a color by making the question more explicit: Does the color of milk equal white? In the weakly typed world, we don’t need this refinement.
JavaScript 是弱类型的。我们可以通过使用anyTypeScript 中的类型并推迟到 JavaScript 来处理运行时的输入来看到这一点。JavaScript 提供了两个相等运算符: ==,它检查两个值是否相等,以及===,它检查值和值的类型是否相等,如下一个清单所示。因为 JavaScript 是弱类型的,所以表达式"42" == 42如true. 这是令人惊讶的,因为"42"是文本,而42是数字。
JavaScript is weakly typed. We can see this by using the any type in TypeScript and deferring to JavaScript to handle typing at run time. JavaScript provides two equality operators: ==, which checks whether two values are equal, and ===, which checks both that the values and the type of the values are equal, as shown in the next listing. Because JavaScript is weakly typed, an expression such as "42" == 42 evaluates to true. This is surprising, because "42" is text, whereas 42 is a number.
const a: any = "你好世界";
常量 b: 任何 = 42;
控制台日志(a == b); 1个
console.log("42" == b); 2个
console.log("42" === b); 3个const a: any = "hello world";
const b: any = 42;
console.log(a == b); 1
console.log("42" == b); 2
console.log("42" === b); 3
隐式类型转换很方便,因为我们不必编写更多代码来在类型之间进行显式转换,但它们很危险,因为在许多情况下我们不希望发生转换并且对结果感到惊讶。aTypeScript 是强类型的,当我们正确声明为 astring和b为 a时,它不会编译前面的任何比较number,如以下清单所示。
Implicit type conversions are handy in that we don’t have to write more code to explicitly convert between types, but they are dangerous because in many cases we do not want conversions to happen and are surprised by the results. TypeScript, being strongly typed, doesn’t compile any of the preceding comparisons when we properly declare a to be a string and b to be a number, as the following listing shows.
const a: string =c"你好世界"; 1
const b:数字= 42; 1个
控制台日志(a == b); 2个
2
console.log("42" == b); 2个
2
console.log("42" === b); 2个const a: string =c"hello world"; 1
const b: number = 42; 1
console.log(a == b); 2
2
console.log("42" == b); 2
2
console.log("42" === b); 2
现在所有的比较都会导致错误"This condition will always return 'false' since the types 'string' and 'number' have no overlap"。类型检查器确定我们正在尝试比较不同类型的值并拒绝该代码。
All the comparisons now cause the error "This condition will always return 'false' since the types 'string' and 'number' have no overlap". The type checker determines that we are trying to compare values of different types and rejects the code.
虽然弱类型系统在短期内更容易使用,因为它不会强制程序员在类型之间显式转换值,但它不会提供我们从更强类型系统获得的相同保证。本章中描述的大部分好处以及本书其余部分中采用的技术如果没有得到正确执行,就会失去效力。
Although a weak type system is easier to work with in the short term, as it doesn’t force programmers to explicitly convert values between types, it does not provide the same guarantees we get from a stronger type system. Most of the benefits described in this chapter and the techniques employed in the rest of this book lose their effectiveness if they are not properly enforced.
请注意,尽管类型系统是动态的(运行时类型检查)或静态的(编译时类型检查),但它的优势在于一个范围:它执行的隐式转换越多,它就越弱。大多数类型系统,即使是强大的类型系统,也确实为被认为安全的转换提供了一些有限的隐式转换。一个常见的例子是转换为boolean:if (a)在大多数语言中即使a是 anumber或引用类型也会编译。另一个例子是加宽转换,我们将在第 4 章中详细介绍。TypeScript 仅使用number类型来表示数值,但在某些语言中,例如,我们需要一个 16 位整数,但传入一个8 位整数,转换通常自动完成,因为没有数据损坏的风险。(16 位整数可以表示 8 位整数可以表示的任何值,甚至更多。)
Note that although a type system is either dynamic (type checking at run time) or static (type checking at compile time), its strength lies on a spectrum: the more implicit conversions it performs, the weaker it is. Most type systems, even strong ones, do provide some limited implicit casting for conversions that are deemed safe. A common example is conversions to boolean: if (a) in most languages would compile even if a is a number or a reference type. Another example is widening casts, which we’ll cover in detail in chapter 4. TypeScript uses only the number type to represent numeric values, but in languages in which, for example, we need a 16-bit integer but pass in an 8-bit integer, the conversion is usually done automatically, as there is no risk of data corruption. (A 16-bit integer can represent any value that an 8-bit integer can, and more.)
在某些情况下,编译器可以推断出变量或函数的类型,而无需我们明确指定。42例如,如果我们将值赋给一个变量,TypeScript 编译器可以推断出它的类型是number,因此我们不需要提供类型符号。如果我们想要明确并使代码的读者清楚类型,我们可以这样做,但符号并不是严格要求的。
In some cases, the compiler can infer the type of a variable or a function without us having to specify it explicitly. If we assign the value 42 to a variable, for example, the TypeScript compiler can infer that its type is number, so we don’t need to provide the type notations. We can do so if we want to be explicit and make the type clear to readers of the code, but the notation is not strictly required.
同样,如果一个函数在每条return语句中返回一个相同类型的值,我们不需要在函数定义中明确说明它的返回类型。编译器可以从代码中推断出它,如下一个清单所示。
Similarly, if a function returns a value of the same type on each return statement, we don’t need to spell out its return type explicitly in the function definition. The compiler can infer it from the code, as shown in the next listing.
函数添加(x:数字,y:数字){ 1
返回 x + y;
}
让 sum = add(40, 2); 2个function add(x: number, y: number) { 1
return x + y;
}
let sum = add(40, 2); 2
与仅在运行时执行类型的动态类型不同,在这些情况下,类型仍然在编译时确定和检查,但我们不必显式提供它。如果键入不明确,编译器将发出错误并要求我们通过提供类型符号来更明确。
Unlike dynamic typing, in which typing is performed only at run time, in these cases the typing is still determined and checked at compile time, but we don’t have to supply it explicitly. If typing is ambiguous, the compiler will issue an error and ask us to be more explicit by providing type notations.
强大的静态类型系统使我们能够编写更正确、更可组合和更易读的代码。本书将涵盖此类现代类型系统的常见特征,重点是这些特征的实际应用。
A strong, static type system enables us to write code that is more correct, more composable, and more readable. This book will cover common features of such modern type systems with a focus on practical applications of these features.
我们将从原始类型开始,这是大多数语言中可用的现成类型。我们将介绍如何正确使用它们并避免一些常见的陷阱。在某些情况下,如果您的特定语言本身不提供这些类型,我们会展示如何实现其中一些类型。
We’ll start with primitive types, the out-of-the-box types available in most languages. We’ll cover using them correctly and avoiding some common pitfalls. In some cases, we show how to implement some of these types if your particular language does not provide them natively.
接下来,我们将研究组合以及如何将基本类型放在一起以构建支持您的特定问题域的大型类型。组合类型的方法有多种,因此您将了解如何根据要解决的特定问题为工作选择正确的工具。
Next, we’ll look at composition and how primitive types can be put together to build a large universe of types supporting your particular problem domain. There are multiple ways to combine types, so you’ll learn how to pick the right tool for the job depending on the particular problem you are trying to solve.
然后我们将介绍函数类型和当类型系统可以类型化函数并将它们视为常规值时向我们开放的新实现。函数式编程是一个非常深奥的话题,因此我们不会尝试全面解释它,而是借用一组有用的概念并将它们应用到非函数式语言中以解决现实世界中的问题。
Then we will cover function types and the new implementations that open to us when a type system can type functions and treat them as regular values. Functional programming is a very deep topic, so instead of attempting to explain it fully, we’ll borrow a set of useful concepts and apply them to a nonfunctional language to solve real-world problems.
在能够键入值、组合类型和类型函数之后,类型系统进化的下一步是子类型化。我们将回顾是什么使一种类型成为另一种类型的子类型,并了解如何将一些面向对象的编程概念应用到我们的代码中。我们将讨论继承、组合和不太传统的混合。
The next step in the evolution of type systems, after being able to type values, compose types, and type functions, is subtyping. We’ll go over what makes a type a subtype of another type and see how we can apply some object-oriented programming concepts to our code. We’ll discuss inheritance, composition, and the less-traditional mix-ins.
我们将继续使用泛型,它启用类型变量并允许我们对类型的代码进行参数化。泛型打开了一个全新的抽象和可组合性级别,将数据与数据结构、数据结构与算法解耦,并启用自适应算法。
We’ll continue with generics, which enable type variables and allow us to parameterize code on types. Generics open a whole new level of abstraction and composability, decoupling data from data structures, data structures from algorithms, and enabling adaptive algorithms.
最后,我们将介绍更高种类的类型,它们是下一个抽象级别,参数化泛型类型。更高种类的类型将数据结构形式化,例如幺半群和单子。今天许多编程语言不支持更高种类的类型,但它们在 Haskell 等语言中的广泛使用和日益流行最终将导致它们在更成熟的语言中被采用。
Last, we’ll cover higher kinded types, which are the next level of abstraction, parameterizing generic types. Higher kinded types formalize data structures such as monoids and monads. Many programming languages do not support higher kinded types today, but their extensive use in languages such as Haskell and increasing popularity will eventually lead to their adoption across more established languages.
在第 2 章中,我们将了解原始类型,它们是类型系统的构建块。我们将学习如何避免使用这些类型时出现的一些常见错误,并了解如何从数组和引用构建几乎任何数据结构。
In chapter 2, we will look at primitive types, which are the building blocks of the type system. We’ll learn how to avoid some common mistakes that arise when using these types and see how we can build almost any data structure from arrays and references.
本章涵盖
This chapter covers
计算机在内部将数据表示为位序列。类型赋予这些序列以意义。同时,类型限制了任何数据可以取的可能值的范围。类型系统提供一组原始类型或内置类型以及一组用于组合这些类型的规则。
Computers represent data internally as sequences of bits. Types give meaning to these sequences. At the same time, types restrict the range of possible values any piece of data can take. Type systems provide a set of primitive or built-in types and a set of rules for combining these types.
在本章中,我们将了解一些常用的基本类型(空、单元、布尔值、数字、字符串、数组和引用)、它们的用途以及需要注意的常见陷阱。尽管我们每天都在使用基本类型,但每种类型都有细微差别,我们必须注意才能有效地使用它们。例如,布尔表达式可以短路,数值表达式可以溢出。
In this chapter we will look at some of the commonly available primitive types (empty, unit, Booleans, numbers, strings, arrays, and references), their uses, and common pitfalls to be aware of. Although we use primitive types every day, each comes with subtle nuances we must be aware of to use them effectively. Boolean expressions can be short-circuited, for example, and numerical expressions can overflow.
我们将从一些最简单的类型开始,这些类型携带很少或没有信息,然后转向通过各种编码表示数据的类型。最后,我们来看看数组和引用,它们是所有其他更复杂数据结构的构建块。
We’ll start with some of the simplest types, which carry little or no information, and move on to types that represent data via various encodings. Finally, we’ll look at arrays and references, which are building blocks for all other more-complex data structures.
将类型视为可能值的集合,您可能想知道是否存在表示空集的类型。空集没有元素,所以这将是一个我们永远无法为其创建实例的类型。这样的类型有用吗?
Viewing types as sets of possible values, you may wonder whether there is a type to represent the empty set. The empty set has no elements, so this would be a type for which we can never create an instance. Would such a type be useful?
作为实用程序库的一部分,让我们看看如何定义一个函数,该函数在给定消息的情况下记录发生错误的事实,包括时间戳和消息,然后抛出异常,如下一个清单所示。这样的函数是 的包装器throw,因此它并不意味着返回值。
As part of a utility library, let’s see how we would define a function that, given a message, logs the fact that an error occurred, including a timestamp and the message, and then throws an exception, as shown in the next listing. Such a function is a wrapper over throw, so it is not meant to return a value.
const fs = require("fs");
函数 raise(message: string): never { 1
console.error(`错误“${message}”在 ${new Date()} 处引发);
抛出新的错误(消息);
}
函数 readConfig(configFile: string): string {
if (!fs.existsSync(configFile)) 2
raise(`配置文件 ${configFile} 丢失`); 3个
返回 fs.readFileSync(configFile, "utf-8");
}const fs = require("fs");
function raise(message: string): never { 1
console.error(`Error "${message}" raised at ${new Date()}`);
throw new Error(message);
}
function readConfig(configFile: string): string {
if (!fs.existsSync(configFile)) 2
raise(`Configuration file ${configFile} missing`); 3
return fs.readFileSync(configFile, "utf-8");
}
请注意,示例中函数的返回类型是never. 这让代码的读者清楚地知道raise()永远不会返回。更好的是,如果后来有人不小心更新了该函数并使其返回,则代码将不再编译。绝对不能给 赋值never,因此编译器确保该函数保持设计的行为并且永不返回。
Note that the return type of the function in the example is never. This makes it clear to readers of the code that raise() is never meant to return. Even better, if someone accidentally updates the function later and makes it return, the code no longer compiles. Absolutely no value can be assigned to never, so the compiler ensures that the function keeps behaving as designed and never returns.
这样的类型被命名为无法居住的类型或空类型,因为无法创建它的实例。
Such a type is named an uninhabitable type or empty type because no instance of it can be created.
空类型是不能有任何值的类型:它的可能值集是空集。我们永远不能实例化这种类型的变量。我们使用空类型来表示不可能,例如将它用作永不返回(永远抛出或循环)的函数的返回类型。
An empty type is a type that cannot have any value: its set of possible values is the empty set. We can never instantiate a variable of such a type. We use an empty type to denote impossibility, such as by using it as the return type of a function that never returns (throws or loops forever).
不可居住的类型用于声明永不返回的函数。一个函数可能不会返回有几个原因:它可能在所有代码路径上抛出异常,它可能永远循环,或者它可能使程序崩溃。所有这些场景都是有效的。我们可能希望实现一个函数,在发生不可恢复的错误时抛出异常或崩溃之前执行一些日志记录或发送一些遥测数据。我们可以拥有希望在整个系统关闭之前不断循环运行的代码,例如系统的事件处理循环。
An uninhabitable type is used to declare a function that never returns. A function might not return for several reasons: it might throw an exception on all code paths, it might loop forever, or it might crash the program. All these scenarios are valid. We might want to implement a function that does some logging or sends some telemetry before throwing an exception or crashing in case of unrecoverable error. We can have code that we want to run continuously on a loop until the whole system is shut down, such as the event-processing loop of the system.
将这样的函数声明为 returning 是一种void误导,这是大多数编程语言用来指示缺少有意义值的类型。我们的函数不仅不返回有意义的值,而且根本不返回!
Declaring such a function as returning void, which is the type used by most programming languages to indicate the absence of a meaningful value, is misleading. Our function not only doesn’t return a meaningful value, but also doesn’t return at all!
空类型可能看起来微不足道,但它显示了数学和计算机科学之间的根本区别:在数学中,我们不能定义一个从非空集到空集的函数。这根本没有意义。数学中的函数不是“求值”的;他们只是“是”。
The empty type might seem trivial, but it shows a fundamental difference between mathematics and computer science: in mathematics, we cannot define a function from a nonempty set to an empty set. This simply doesn’t make sense. Functions in mathematics are not “evaluated”; they simply “are.”
另一方面,计算机评估程序;他们一步一步地执行指令。计算机最终可能会评估一个无限循环,这意味着它们永远不会停止执行。出于这个原因,计算机程序可以为空集定义一个有意义的函数,如前面的示例所示。
Computers, on the other hand, evaluate programs; they execute instructions step by step. Computers can end up evaluating an infinite loop, which means that they would never stop their execution. For this reason, a computer program can define a meaningful function to the empty set, as in the preceding examples.
每当你有一个非返回函数或者想明确表明它不可能有一个值时,考虑使用空类型。
Consider using an empty type whenever you have a nonreturning function or otherwise want to explicitly show that it’s impossible to have a value.
并非所有主流语言都像 TypeScript 那样提供内置的空类型never,但您可以在大多数语言中实现一个。您可以通过定义一个没有元素的枚举或一个只有私有构造函数的结构来实现这一点,这样它就永远不会被调用。
Not all mainstream languages provide a built-in empty type like never in TypeScript, but you can implement one in most of them. You can do this by defining an enumeration with no elements or a structure with only a private constructor such that it can never be called.
清单 2.2显示了我们如何在 TypeScript 中将一个空类型实现为一个无法实例化的类。请注意,如果两种类型具有相似的结构,TypeScript 认为它们是兼容的,因此我们需要添加一个虚拟void属性以确保其他代码不会以可以键入为Empty. 其他语言(例如 Java 和 C#)不需要此附加属性,因为它们不会认为类型基于形状是兼容的。我们将在第 7 章中更详细地介绍这一点。
Listing 2.2 shows how we would implement an empty type in TypeScript as a class that can’t be instantiated. Note that TypeScript considers two types to be compatible if they have similar structure, so we need to add a dummy void property to ensure that other code cannot end up with a value that can be typed as Empty. Other languages, such as Java and C#, would not need this additional property, as they wouldn’t consider types to be compatible based on shape. We’ll cover this in more detail in chapter 7.
声明 const EmptyType:唯一符号; 1个
类空{
[空类型]: void; 1
私有构造函数() { } 3
}
function raise(message: string):空{ 3
console.error(`错误“${message}”在 ${new Date()} 处引发);
抛出新的错误(消息);
}declare const EmptyType: unique symbol; 1
class Empty {
[EmptyType]: void; 1
private constructor() { } 3
}
function raise(message: string): Empty { 3
console.error(`Error "${message}" raised at ${new Date()}`);
throw new Error(message);
}
代码编译,因为编译器执行控制流分析并确定不需要return语句。另一方面,添加return语句应该是不可能的,因为我们不能创建Empty.
The code compiles, as the compiler performs control flow analysis and determines no return statement is needed. On the other hand, it should be impossible to add a return statement, as we cannot create an instance of Empty.
在上一节中,我们研究了永不返回的函数。返回但不返回任何有用信息的函数呢?有很多这样的函数,我们调用它们只是为了它们的副作用:它们做了一些事情,改变了一些外部状态,但不执行任何有用的计算来返回给我们。
In the previous section, we looked at functions that never return. What about functions that do return but don’t return anything useful? There are many functions like this, which we call only for their side effects: they do something, change some external state, but don’t perform any useful computation to return to us.
让我们举console.log()个例子:它把它的参数输出到调试控制台,但不返回任何有意义的东西。另一方面,该函数在完成执行时确实会将控制权返回给调用者,因此它的返回类型不能是never.
Let’s take console.log() as an example: it outputs its argument to the debug console, but doesn’t return anything meaningful. On the other hand, the function does return control to the caller when it finishes executing, so its return type can’t be never.
"Hello world!"下一个清单中显示的经典函数是另一个很好的例子。我们称它为打印问候语(这是副作用),而不是返回值,因此我们将其返回值指定为void.
The classic "Hello world!" function shown in the next listing is another good example. We call it to print a greeting (which is a side effect), not to return a value, so we specify its return value as void.
功能问候():无效{ 1
console.log("你好世界!");
}
迎接(); 2个function greet(): void { 1
console.log("Hello world!");
}
greet(); 2
这种函数的返回类型称为unit type,一种只允许一个值的类型,它在 TypeScript 和大多数其他语言中的名称是void。我们通常没有类型变量void并且可以简单地从void函数返回而不提供实际值的原因是单元类型的值并不重要。
The return type of such a function is called a unit type, a type that allows just one value, and its name in TypeScript and most other languages is void. The reason why we usually don’t have variables of type void and can simply return from a void function without providing an actual value is that the value of a unit type is not important.
单位类型是一种只有一个可能值的类型。如果我们有一个这种类型的变量,那么检查它的值就没有意义了;它只能是一个值。当函数的结果没有意义时,我们使用单位类型。
A unit type is a type that has only one possible value. If we have a variable of such a type, there is no point in checking its value; it can only be the one value. We use unit types when the result of a function is not meaningful.
接受任意数量的参数但不返回任何有意义的值的函数也称为动作(因为它们通常执行一个或多个改变世界状态的操作)或消费者(因为参数输入但没有输出)。
Functions that take any number of arguments but don’t return any meaningful value are also called actions (because they usually perform one or more operations that change the state of the world) or consumers (because arguments go in but nothing comes out).
尽管大多数编程语言都提供 like 类型void,但某些语言void以特殊方式处理,可能不允许您以与任何其他类型完全相同的方式使用它。在这种情况下,您可以通过定义具有单个元素的枚举或没有状态的单例来创建自己的单元类型。因为一个单位类型只有一个可能的值,所以那个值是什么并不重要;所有单位类型都是等价的。从一种单位类型转换为另一种单位类型很简单,因为没有选择:一种类型的单个值映射到另一种类型的单个值。
Although a type like void is available in most programming languages, some languages treat void in a special way and may not allow you to use it exactly the same way as any other type. In such situations, you can create your own unit type by defining an enumeration with a single element or a singleton without state. Because a unit type has only one possible value, it doesn’t really matter what that value is; all unit types are equivalent. It’s trivial to convert from one unit type to another, as there is no choice to be made: the single value of one type maps to the single value of the other one.
清单 2.4展示了我们如何在 TypeScript 中实现一个单元类型。至于 DIY 空类型,我们正在使用一个void属性来确保另一个具有兼容结构的类型不会被隐式转换为Unit. 其他语言(如 Java 和 C#)不需要此附加属性。
Listing 2.4 shows how we would implement a unit type in TypeScript. As for the DIY empty type, we are using a void property to ensure that another type with a compatible structure is not implicitly converted to Unit. Other languages, such as Java and C#, would not need this additional property.
声明 const UnitType:唯一符号;
类单位{
[单位类型]:无效; 1 个
静态只读值:Unit = new Unit(); 2
私有构造函数(){}; 3个
}
功能问候():单位{ 4
console.log("你好世界!");
返回单位值; 4
}declare const UnitType: unique symbol;
class Unit {
[UnitType]: void; 1
static readonly value: Unit = new Unit(); 2
private constructor() { }; 3
}
function greet(): Unit { 4
console.log("Hello world!");
return Unit.value; 4
}
set()接受一个值并将其分配给全局变量的函数 的返回类型应该是什么?
- never
- undefined
- void
- any
What should be the return type of a set() function that takes a value and assigns it to a global variable?
- never
- undefined
- void
- any
terminate()立即停止程序执行的函数 的返回类型应该是什么?
What should be the return type of a terminate() function that immediately stops execution of the program?
在没有可能值的类型(空类型,例如never)和具有一个可能值的类型(单元类型,例如void)之后,是具有两个可能值的类型。在大多数编程语言中可用的规范二值类型是布尔类型。
After types with no possible values (empty types such as never) and types with one possible value (unit types such as void), come types with two possible values. The canonical two-valued type, available in most programming languages, is the Boolean type.
布尔值编码真实性。这个名字来自 George Boole,他介绍了现在所谓的布尔代数,一种由真值 (1) 和假值 (0) 以及对它们的逻辑运算(例如 、 和 )组成AND的OR代数NOT。
Boolean values encode truthiness. The name comes from George Boole, who introduced what is now called Boolean algebra, an algebra consisting of truth (1) and falseness (0) values and logical operations on them such as AND, OR, and NOT.
一些类型系统提供布尔值作为内置类型,其值true和false. 其他系统依赖于数字,认为 0 代表意义false,任何其他数字代表意义true(也就是说,任何不是 false 的都是 true)。booleanTypeScript 有一个带有可能值的内置类型true和false.
Some type systems provide Booleans as a built-in type with values true and false. Other systems rely on numbers, considering 0 to mean false and any other number to mean true (that is, whatever is not false is true). TypeScript has a built-in boolean type with possible values true and false.
无论原始布尔类型是否存在,或者真实值是从其他类型的值中推断出来的,大多数编程语言都使用某种形式的布尔语义来启用条件分支。if (condition) { ... }只有当条件的计算结果为真时,诸如 的语句才会执行大括号之间的部分。循环依赖于条件来确定是迭代还是完成:while (condition) { ... }。没有条件分支,我们将无法编写非常有用的代码。想想你将如何实现一个非常简单的算法,例如在数字列表中找到第一个偶数,而不需要任何循环或条件语句。
Regardless of whether a primitive Boolean type exists or truthiness values are inferred from values of other types, most programming languages use some form of Boolean semantics to enable conditional branching. A statement such as if (condition) { ... } will execute the part between curly brackets only if the condition evaluates to something true. Loops rely on conditions to determine whether to iterate or finish: while (condition) { ... }. Without conditional branching, we wouldn’t be able to write very useful code. Think about how you would implement a very simple algorithm, such as finding the first even number in a list of numbers, without any loops or conditional statements.
许多编程语言对常见的布尔运算使用以下符号:&&for AND、||forOR和!for NOT。布尔表达式通常用真值表来描述(图 2.1)。
Many programming languages use the following symbols for common Boolean operations: && for AND, || for OR, and ! for NOT. Boolean expressions are usually described with truth tables (figure 2.1).
假设您必须为评论系统构建一个网守,如清单 2.5所示:当用户尝试发表评论时,网守拒绝彼此相隔 10 秒内发布的评论(用户在发送垃圾邮件)和内容为空的评论(用户不小心在键入任何内容之前单击评论)。
Suppose that you must build a gatekeeper for a commenting system as shown in listing 2.5: as users attempt to post comments, the gatekeeper rejects comments posted within 10 seconds of each other (the user is spamming) and comments with empty contents (the user accidentally clicked Comment before typing anything).
看门人函数将评论和用户 ID 作为参数。您secondsSinceLastComment()已经实现了一个功能;此函数在给定用户 ID 的情况下查询数据库并返回自上次发布以来的秒数。
The gatekeeper function takes as arguments the comment and the user ID. You have a secondsSinceLastComment() function already implemented; this function, given the user ID, queries the database and returns the number of seconds since the last post.
如果两个条件都满足,则将评论发布到数据库;如果不是,返回false。
If both conditions are met, post the comment to the database; if not, return false.
声明函数 secondsSinceLastComment(userId: string): number; 1
声明函数 postComment(comment: string, userId: string): void; 2个
function commentGatekeeper(comment: string, userId: string): 布尔值{
如果((secondsSinceLastComment(userId)<10)||(评论==“”)) 3
返回假;
postComment(评论, userId);
返回真;
}declare function secondsSinceLastComment(userId: string): number; 1
declare function postComment(comment: string, userId: string): void; 2
function commentGatekeeper(comment: string, userId: string): boolean {
if ((secondsSinceLastComment(userId) < 10) || (comment == "")) 3
return false;
postComment(comment, userId);
return true;
}
清单 2.5是网守的一个可能实现。请注意如果最后一条评论的年龄(以秒为单位)小于 10或当前评论为空, OR我们返回的表达式。false
Listing 2.5 is a possible implementation of the gatekeeper. Note the OR expression where we return false if either the age of the last comment in seconds is less than 10 or the current comment is empty.
实现相同逻辑的另一种方法是切换两个操作数,如以下清单所示。首先检查当前评论是否为空;然后检查最后发表评论的年龄,如清单 2.5所示。
Another way to implement the same logic is to switch the two operands, as shown in the following listing. First check whether the current comment is empty; then check the age of the last posted comment, as in listing 2.5.
声明函数 secondsSinceLastComment(userId: string): number;
声明函数 postComment(comment: string, userId: string): void;
function commentGatekeeper(comment: string, userId: string): 布尔值{
if ( (comment == "") || (secondsSinceLastComment(userId) < 10) ) 1
返回假;
postComment(评论, userId);
返回真;
}declare function secondsSinceLastComment(userId: string): number;
declare function postComment(comment: string, userId: string): void;
function commentGatekeeper(comment: string, userId: string): boolean {
if ((comment == "") || (secondsSinceLastComment(userId) < 10)) 1
return false;
postComment(comment, userId);
return true;
}
一个版本在任何方面都比另一个更好吗?它们定义了相同的检查——只是顺序不同。事实证明,它们是不同的。根据收到的输入,它们在运行时的行为因布尔表达式的计算方式而异。
Is one version better in any way than the other? They define the same checks—just in a different order. As it turns out, they are different. Depending on the input received, they behave differently at run time due to the way Boolean expressions are evaluated.
大多数编译器和运行时为布尔表达式执行称为短路的优化。形式的表达a AND b被翻译成if a then b else false. 这符合真值表AND:如果第一个操作数为假,则无论第二个操作数是什么,整个表达式都是假的。另一方面,如果第一个操作数为真,则如果第二个操作数也为真,则整个表达式为真。
Most compilers and run times perform an optimization called short circuit for Boolean expressions. Expressions of the form a AND b are translated to if a then b else false. This respects the truth table for AND: if the first operand is false, then regardless of what the second operand is, the whole expression is false. On the other hand, if the first operand is true, then the whole expression is true if the second operand is also true.
类似的翻译发生在a OR b,变成if a then true else b。查看 的真值表OR,如果第一个操作数为真,则无论第二个操作数是什么,整个表达式都为真;否则,如果第一个操作数为假,则如果第二个操作数为真,则表达式为真。
A similar translation happens for a OR b, which becomes if a then true else b. Looking at the truth table for OR, if the first operand is true, then the whole expression is true regardless of what the second operand is; otherwise, if the first operand is false, then the expression is true if the second operand is true.
这种翻译和名称短路的原因来自这样一个事实,即如果评估第一个操作数提供了足够的信息来评估整个表达式,则根本不会评估第二个操作数。看门人功能必须执行两项检查:一项相对便宜的检查,以确保它收到的评论不为空;另一项可能代价高昂的检查,涉及查询评论数据库。在清单 2.5中,数据库查询首先发生。如果最近发表的评论超过 10 秒,短路甚至不会查看当前评论,只会返回false。在清单 2.6中,如果当前评论为空,则不会查询数据库。第二个版本可能会通过评估便宜的检查来跳过昂贵的检查。
The reason for this translation and the name short circuit come from the fact that if evaluating the first operand provides enough information to evaluate the whole expression, the second operand is not evaluated at all. The gatekeeper function must perform two checks: a relatively inexpensive one, to make sure that the comment it receives is not empty, and a potentially expensive one, which involves querying the comment database. In listing 2.5, the database query happens first. If the last posted comment is more recent than 10 seconds, short-circuiting will not even look at the current comment and will simply return false. In listing 2.6, if the current comment is empty, the database doesn’t get queried. The second version can potentially skip an expensive check by evaluating a cheap check.
布尔表达式求值的这个属性很重要,在组合条件时要记住:短路可以跳过右侧表达式的求值,这取决于左侧表达式的求值结果,因此优先选择从最便宜到最贵的。
This property of Boolean expression evaluation is important and something to remember when you are combining conditions: short-circuiting can skip evaluation of the expression on the right, depending on the result of evaluating the expression on the left, so prefer ordering conditions from cheapest to most expensive.
以下代码将打印什么?
让计数器:数字= 0; 函数条件(值:布尔值):布尔值{ 计数器++; 返回值; } 如果(条件(假)&&条件(真)){ // ... } 控制台日志(计数器)
- 0
- 1个
- 2个
- 没有什么; 它抛出一个错误。
What will the following code print?
let counter: number = 0; function condition(value: boolean): boolean { counter++; return value; } if (condition(false) && condition(true)) { // ... } console.log(counter)
- 0
- 1
- 2
- Nothing; it throws an error.
在大多数编程语言中,数字通常作为一种或多种原始类型提供。在处理数字时,您应该注意几个陷阱。举个例子,一个简单的函数可以计算购物总额。如果一个用户以每支 10 美分的价格购买三支泡泡糖,我们预计总计为 30 美分。根据我们使用数字类型的方式,我们可能会感到惊讶。
Numbers are usually provided as one or more primitive types in most programming languages. There are several gotchas you should be aware of when working with numbers. Take, for example, a simple function that adds up a shopping total. If a user purchases three sticks of bubble gum at 10 cents each, we would expect the total to be 30 cents. Depending on how we use numerical types, we might be in for a surprise.
type Item = { name: string, price: number }; 1个
函数 getTotal(items: Item[]): number { 2
让总计:数字= 0;
对于(让项目的项目){
总计 += item.price;
}
返回总计;
}
让总计:数字= getTotal(
[{ 名称:“樱桃泡泡糖”,价格:0.10 }, 3
{ 名称:“薄荷泡泡糖”,价格:0.10 }, 3
{ 名称:“草莓泡泡糖”,价格:0.10 }] 3
);
控制台日志(总计 == 0.30); 4个type Item = { name: string, price: number }; 1
function getTotal(items: Item[]): number { 2
let total: number = 0;
for (let item of items) {
total += item.price;
}
return total;
}
let total: number = getTotal(
[{ name: "Cherry bubblegum", price: 0.10 }, 3
{ name: "Mint bubblegum", price: 0.10 }, 3
{ name: "Strawberry bubblegum", price: 0.10 }] 3
);
console.log(total == 0.30); 4
为什么 0.10 三次相加得不到 0.30?要理解这一点,我们需要看看计算机是如何表示数字类型的。数字类型的两个定义特征是它的宽度和它的编码。
Why does adding up 0.10 three times not give us 0.30? To understand this, we need to look at how numerical types are represented by computers. The two defining characteristics of a numerical type are its width and its encoding.
宽度是用来表示一个值的位数。这可以从 8 位(一个字节)甚至 1 位到 64 位或更多。位宽与底层芯片架构有很大关系:64 位 CPU 具有 64 位寄存器,因此可以对 64 位值进行极快的操作。对给定宽度的数字进行编码有三种常用方法:unsigned binary、two's complement和IEEE 754。
The width is the number of bits used to represent a value. This can range from 8 bits (a byte) or even 1 bit up to 64 bits or more. Bit widths have a lot to do with the underlying chip architecture: a 64-bit CPU has 64-bit registers, thus allowing extremely fast operations on 64-bit values. There are three common ways to encode numbers of a given width: unsigned binary, two’s complement, and IEEE 754.
无符号二进制编码使用每一位来表示部分值。例如,一个 4 位无符号整数可以表示从0到 的任何值15。通常,一个N位无符号整数可以表示从0(所有位都是0)到(所有位都是)的值。图 2.2显示了 4 位无符号整数的几个可能值。您可以使用公式 b N –1 * 2 N–1 + b N–2 * 2 N将N个二进制数字序列( b N –1 b N–2 ...b 1 b 0 ) 转换为十进制数–2 + ... + b 12N-11* 2 1 + b 0 * 2 0。
An unsigned binary encoding uses every bit to represent part of the value. A 4-bit unsigned integer, for example, can represent any value from 0 to 15. In general, an N-bit unsigned integer can represent values from 0 (all bits are 0) up to 2N-1 (all bits are 1). Figure 2.2 shows a few possible values of a 4-bit unsigned integer. You can convert a sequence of N binary digits (bN–1bN–2...b1b0) to a decimal number with the formula bN–1 * 2N–1 + bN–2 * 2N–2 + ... + b1 * 21 + b0 * 20.
这种编码非常简单,但只能表示正数。如果我们还想表示负数,我们需要不同的编码,通常是二进制补码。在二进制补码编码中,我们保留一个位来对符号进行编码。正数与以前完全相同,而负数通过从 2 N中减去它们的绝对值来编码,其中N是位数。图 2.3显示了 4 位有符号整数的几个可能值。
This encoding is very straightforward but can represent only positive numbers. If we also want to represent negative numbers, we need a different encoding, which is usually two’s complement. In two’s complement encoding, we reserve a bit to encode the sign. Positive numbers are represented exactly as before, whereas negative numbers are encoded by subtracting their absolute value from 2N, where N is the number of bits. Figure 2.3 shows a few possible values of a 4-bit signed integer.
使用这种编码,所有负数都有第一位1,所有正数和 0 都有第一位0。一个 4 位有符号整数可以表示从–8到 的值7。我们用来表示一个值的位数越多,我们可以表示的值范围就越大。
With this encoding, all negative numbers have the first bit 1, and all positive numbers and 0 have the first bit 0. A 4-bit signed integer can represent values from –8 to 7. The more bits we use to represent a value, the larger the value range we can represent.
但是,当算术运算的结果不能用给定的位数表示时会发生什么?如果我们使用 4 位无符号编码并尝试添加 10 + 10 怎么办,即使我们可以用 4 位表示的最大值是15?
What happens, though, when the result of an arithmetic operation can’t be represented within the given number of bits? What if we are using a 4-bit unsigned encoding and try to add 10 + 10, even though the maximum value we can represent in 4 bits is 15?
这种情况称为算术溢出。相反的情况,即我们最终得到的数字太小而无法表示,称为算术下溢。不同的语言以不同的方式处理这些情况(图 2.4)。
Such a situation is called an arithmetic overflow. The opposite situation, in which we end up with a number that is too small to represent, is called an arithmetic underflow. Different languages treat these situations in different ways (figure 2.4).
处理算术上溢和下溢的三种主要方法是回绕、饱和或错误输出。
The three main ways to handle arithmetic overflow and underflow are to wrap around, saturate, or error out.
环绕是硬件通常所做的,因为它只是丢弃不适合的位。对于一个 4 位无符号整数,如果位是1111并且我们加 1,结果是10000,但是因为只允许 4 位,一个被丢弃,我们最终得到0000,回到0。这是处理溢出的最有效方法,但也是最危险的方法,因为它可能导致意外结果。将 1 美元加到我的 15 美元上,我最终可以得到 0 美元。
Wrap around is what the hardware usually does, as it simply discards the bits that don’t fit. For a 4-bit unsigned integer, if the bits are 1111 and we add 1, the result is 10000, but because only 4 bits are allowed, one gets discarded, and we end up with 0000, wrapping back around to 0. This is the most efficient way to handle overflow but also the most dangerous, as it can cause unexpected results. Adding $1 to my $15, I can end up with $0.
饱和度是另一种处理溢出的方法。如果一个操作的结果超过了最大可表示值,我们就简单地停在最大值处。这很好地映射到物理世界:如果你的恒温器只上升到某个温度,试图让它变暖并不会改变它。另一方面,使用饱和,算术运算不再总是关联的。如果7是我们的最大值,则 7 + (2 – 2) = 7 + 0 = 7 但 (7 + 2) – 2 = 7 – 2 = 5。
Saturation is another way to handle overflow. If the result of an operation exceeds the maximum representable value, we simply stop at the maximum. This maps well to the physical world: if your thermostat only goes up to some temperature, trying to make it warmer won’t change that. On the other hand, using saturation, arithmetic operations are no longer always associative. If 7 is our maximum value, 7 + (2 – 2) = 7 + 0 = 7 but (7 + 2) – 2 = 7 – 2 = 5.
第三种可能性error out是在发生溢出时抛出错误。这是最安全的方法,但缺点是需要检查每一个算术运算,并且无论何时执行任何算术,您的代码都需要处理异常情况。
The third possibility, error out, is to throw an error when an overflow happens. This is the safest approach but has the drawback that every single arithmetic operation needs to be checked, and whenever you perform any arithmetic, your code needs to handle exceptional cases.
根据您使用的语言,算术上溢和下溢可以通过这些方式中的任何一种来处理。如果您的方案需要与语言默认值不同的处理,您需要检查操作是否会溢出或下溢并单独处理该方案。诀窍是在允许值的范围内执行此操作。
Depending on the language you are using, arithmetic overflows and underflows could be handled in any one of these ways. If your scenario requires different handling from the language default, you need to check whether an operation would overflow or underflow and handle that scenario separately. The trick is to do this within the range of allowed values.
例如,要检查添加值是否a会b溢出或下溢 [ MIN, MAX] 范围,我们需要确保我们没有a + b < MIN(添加两个负数时)或a + b > MAX。
To check whether adding values a and b would overflow or underflow a [MIN, MAX] range, for example, we need to ensure that we don’t have a + b < MIN (when adding two negative numbers) or a + b > MAX.
如果b是正数,我们不可能有a + b < MIN,因为我们正在变得a更大,而不是更小。在这种情况下,我们只需要检查溢出。我们可以重写a + b > MAX为a > MAX – b(b两边相减)。因为我们减去一个正数,所以我们正在使值变小,所以不存在溢出的风险(MAX – b在范围内[MIN, MAX])。所以我们溢出 ifb > 0和a > MAX – b。
If b is positive, we can’t possibly have a + b < MIN, as we’re making a bigger, not smaller. In this case, we only need to check for overflow. We can rewrite a + b > MAX as a > MAX – b (subtract b on both sides). Because we’re subtracting a positive number, we are making the value smaller, so there is no risk of overflowing (MAX – b is within the [MIN, MAX] range). So we overflow if b > 0 and a > MAX – b.
如果b是负数,我们不可能有a + b > MAX,因为我们正在变a小,而不是变大。在这种情况下,我们只需要检查下溢。我们可以重写a + b < MIN as a < MIN – b(b两边相减)。因为我们减去一个负数,所以我们正在使值变大,所以不存在下溢的风险(MIN – b在[MIN, MAX]范围内)。所以我们下溢 ifb < 0和a < MIN – b,如下一个清单所示。
If b is negative, we can’t possibly have a + b > MAX, as we’re making a smaller, not bigger. In this case, we only need to check for underflow. We can rewrite a + b < MIN as a < MIN – b (subtract b on both sides). Because we’re subtracting a negative number, we are making the value larger, so there is no risk of underflowing (MIN – b is within the [MIN, MAX] range). So we underflow if b < 0 and a < MIN – b, as shown in the next listing.
函数 addError(a: 数字, b: 数字,
最小值:数字,最大值:数字):布尔值 { 1
如果 (b >= 0) {
返回一个 > 最大 - b; 2个
} 别的 {
返回 a < min - b; 3个
}
}function addError(a: number, b: number,
min: number, max: number): boolean { 1
if (b >= 0) {
return a > max - b; 2
} else {
return a < min - b; 3
}
}
我们可以使用类似的逻辑进行减法。
We can use similar logic for subtraction.
对于乘法,我们通过在两边除以 来检查上溢和下溢b。在这里,我们需要考虑两个数字的符号,因为两个负数相乘得到一个正数,而一个正数和一个负数相乘得到一个负数。
For multiplication, we check for overflow and underflow by dividing on both sides by b. Here, we need to consider the signs of both numbers, as multiplying two negative numbers yields a positive number, whereas multiplying a positive and a negative number yields a negative number.
如果我们溢出
We overflow if
我们下溢如果
We underflow if
对于整数除法, 的值a / b始终是一个整数,其值介于 -a和 之间a。[-a,a]如果不完全在 内,我们只需要检查上溢和下溢[MIN,MAX]。回到我们的 4 位有符号整数示例,其中MINis – 8 和MAXis 7,除法溢出的唯一情况是– 8 / – 1(因为 [ – 8,8] 不完全在 [ – 8,7] 内) . 事实上,对于有符号整数,唯一的溢出情况是什么时候a是最小可表示值并且b是– 1。无符号整数除法永远不会溢出。
For integer division, the value of a / b is always an integer whose value is between - a and a. We only need to check for overflow and underflow if [-a,a] is not fully within [MIN,MAX]. Going back to our 4-bit signed integer example, where MIN is –8 and MAX is 7, the only case where division overflows is –8 / –1 (because [–8,8] is not fully within [–8,7]). In fact, for signed integers, the only overflow scenario is when a is the minimum representable value and b is –1. Unsigned integer division can never overflow.
表 2.1和2.2总结了在需要特殊处理时检查上溢和下溢的必要步骤。
Tables 2.1 and 2.2 summarize the steps necessary to check for overflow and underflow when special handling is required.
|
减法 Subtraction |
乘法 Multiplication |
分配 Division |
|
|---|---|---|---|
| b > 0 且 a > MAX – b | b < 0 和 a > MAX + b | b > 0,a > 0,且 a > MAX / b b < 0,a < 0,且 a < MAX / b | a == MIN 和 b == -1 |
|
添加 Addition |
减法 Subtraction |
乘法 Multiplication |
分配 Division |
|---|---|---|---|
| b < 0 且 a < 最小值 – b | b > 0 且 a < 最小值 + b | b > 0,a < 0,且 a < MIN / b b < 0,a > 0,且 a > MIN / b | 不适用 |
IEEE 754 是电气和电子工程师协会的标准,用于表示浮点数或带小数部分的数字。在 TypeScript(和 JavaScript)中,数字使用binary64编码表示为 64 位浮点数。图 2.5详细说明了这种表示。
IEEE 754 is the Institute of Electrical and Electronics Engineers standard for representing floating-point numbers, or numbers with a fractional part. In TypeScript (and JavaScript), numbers are represented as 64-bit floating-point using the binary64 encoding. Figure 2.5 details this representation.
浮点数的三个组成部分是符号、指数和尾数。符号是用于正数或负数的单个位。尾数是一个分数,如图 2.2中的公式所示。这个分数乘以 2 提高到偏置指数。 01
The three components of a floating-point number are the sign, the exponent, and the mantissa. The sign is a single bit that is 0 for positive numbers or 1 for negative numbers. The mantissa is a fraction as described by the formula in figure 2.2. This fraction is multiplied by 2 raised to the biased exponent.
指数之所以称为有偏差,是因为我们从指数表示的无符号整数中减去一个值,使其既可以表示正数也可以表示负数。在 binary64 的情况下,该值为 1023。IEEE 754 标准定义了几种编码,其中一些使用基数 10 而不是基数 2,尽管基数 2 在实践中出现得更多。
The exponent is called biased because from the unsigned integer represented by the exponent, we subtract a value so that it can represent both positive and negative numbers. In the binary64 case, the value is 1023. The IEEE 754 standard defines several encodings, some using base 10 instead of base 2, though base 2 appears more often in practice.
该标准还定义了特殊值:
The standard also defines special values:
如果需要精度——例如在处理货币时——避免使用浮点数。将 0.10 相加三次不等于 0.30 的原因是,虽然每个单独的 0.10 表示都四舍五入为 0.10,但将它们相加会产生一个四舍五入为 0.30000000000000004 的数字。
If precision is needed—in dealing with currency, for example—avoid using floating-point numbers. The reason why adding 0.10 together three times doesn’t equal 0.30 is that although each individual 0.10 representation gets rounded to 0.10, adding them yields a number that rounds to 0.30000000000000004.
小整数可以安全地表示而无需四舍五入,因此将价格编码为一对美元和美分整数是一个更好的主意。JavaScript 提供Number.isSafeInteger(),它告诉我们是否可以在不舍入的情况下表示整数值,因此依靠它,我们可以设计一个Currency类型来编码两个整数值并防止舍入问题,如下一个清单所示。
Small integer numbers can safely be represented without rounding, so it is a better idea to encode a price as a pair of dollars and cents integers. JavaScript provides Number.isSafeInteger(), which tells us whether an integer value can be represented without rounding, so relying on that, we can design a Currency type that encodes two integer values and protects against rounding issues, as the next listing shows.
类货币{
私人美元:数量; 1
私仙:数; 1个
构造函数(美元:数字,美分:数字){
如果(!Number.isSafeInteger(美元)) 2
throw new Error("不能安全地表示美元金额");
如果 (!Number.isSafeInteger(cents)) 2
throw new Error("不能安全地表示美分金额");
this.dollars = 美元;
this.cents = 美分;
}
getDollars(): 数字 { 3
返回 this.dollars;
}
getCents(): 数字 { 3
返回this.cents;
}
}
函数添加(货币 1:货币,货币 2:货币):货币 {
返回新货币(
currency1.getDollars() + currency2.getDollars(), 4
currency1.getCents() + currency2.getCents()); 4
}class Currency {
private dollars: number; 1
private cents: number; 1
constructor(dollars: number, cents: number) {
if (!Number.isSafeInteger(dollars)) 2
throw new Error("Cannot safely represent dollar amount");
if (!Number.isSafeInteger(cents)) 2
throw new Error("Cannot safely represent cents amount");
this.dollars = dollars;
this.cents = cents;
}
getDollars(): number { 3
return this.dollars;
}
getCents(): number { 3
return this.cents;
}
}
function add(currency1: Currency, currency2: Currency): Currency {
return new Currency(
currency1.getDollars() + currency2.getDollars(), 4
currency1.getCents() + currency2.getCents()); 4
}
在另一种语言中,我们会使用两种整数类型并防止上溢/下溢。因为 JavaScript 不提供整型原始类型,我们依赖于Number.isSafeInteger()防止舍入。在处理货币时,与其让货币神秘地出现或消失,不如出错。
In another language we would’ve used two integer types and protected against overflow/underflow. Because JavaScript does not provide an integer primitive type, we rely on Number.isSafeInteger() to protect against rounding. When dealing with currency, it’s better to error out than to have money mysteriously appear or disappear.
清单 2.9中的类仍然很简单。一个很好的练习是扩展它,以便每 100 美分自动转换为 1 美元。您必须注意在何处检查安全整数:如果美元金额是一个安全整数,但将其加 1(从 100 美分开始)会使它不安全怎么办?
The class in listing 2.9 is still pretty bare-bones. A good exercise is to extend it so that every 100 cents gets automatically converted to a dollar. You must be careful about where to check for safe integers: what if the dollar amount is a safe integer but adding 1 to it (from 100 cents) makes it unsafe?
正如我们所见,由于四舍五入,比较浮点数是否相等通常不是一个好主意。有一种更好的方法可以判断两个值是否大致相同:我们可以确保它们的差异在给定的阈值内。
As we’ve seen, because of rounding, it’s usually not a good idea to compare floating-point numbers for equality. There is a better way to tell whether two values are approximately the same: we can make sure that their difference is within a given threshold.
这个阈值应该是多少?它应该是最大可能的舍入误差。该值称为机器 epsilon,并且是特定于编码的。JavaScript 将此值作为Number.EPSILON. 使用这个值,我们可以实现两个数字之间的相等比较,取它们差的绝对值并检查它是否小于机器 epsilon。如果是,则这些值在彼此的舍入误差范围内,因此我们可以认为它们相等。
What should this threshold be? It should be the maximum possible rounding error. This value is called a machine epsilon and is encoding-specific. JavaScript provides this value as Number.EPSILON. Using this value, we can implement an equality comparison between two numbers, taking the absolute value of their difference and checking whether it is smaller than the machine epsilon. If it is, the values are within rounding error of each other, so we can consider them equal.
函数 epsilonEqual(a:数字,b:数字):布尔值 {
返回 Math.abs(a - b) <= Number.EPSILON; 1个
}
控制台日志(0.1 + 0.1 + 0.1 == 0.3); 2
console.log(epsilonEqual(0.1 + 0.1 + 0.1, 0.3)); 3个function epsilonEqual(a: number, b: number): boolean {
return Math.abs(a - b) <= Number.EPSILON; 1
}
console.log(0.1 + 0.1 + 0.1 == 0.3); 2
console.log(epsilonEqual(0.1 + 0.1 + 0.1, 0.3)); 3
一般来说,epsilonEqual()在比较两个浮点数时使用类似的东西是个好主意,因为算术运算会导致舍入错误,从而导致意外结果。
It’s a good idea in general to use something like epsilonEqual() whenever comparing two floating-point numbers, as arithmetic operations can cause rounding errors that lead to unexpected results.
大多数语言都有提供任意大数字的库。这些类型将它们的宽度扩展到表示任何值所需的尽可能多的位。Python 提供了这样一种类型作为默认的数值类型,BigInt目前提出了一种任意大的类型用于 JavaScript 的标准化。也就是说,我们不会将任意大的数字视为原始类型,因为它们可以由固定宽度的数字类型构建。它们很方便,但许多运行时本身并不提供它们,因为没有等效的硬件。(芯片总是在固定数量的位上运行。)
Most languages have libraries that provide arbitrarily large numbers. These types extend their width to as many bits as needed to represent any value. Python provides such a type as the default numerical type, and an arbitrarily large BigInt type is currently proposed for standardization for JavaScript. That being said, we won’t treat arbitrarily large numbers as primitive types because they can be built out of fixed-width numerical types. They are convenient, but many run times do not provide them natively, as there is no hardware equivalent. (Chips always operate on a fixed number of bits.)
以下代码将打印什么?
让一个:数字= 0.3; 让 b: 数字 = 0.9; 控制台日志(a * 3 == b);
- 没有什么; 它抛出一个错误。
- true
- false
- 0.9
What will the following code print?
let a: number = 0.3; let b: number = 0.9; console.log(a * 3 == b);
- Nothing; it throws an error.
- true
- false
- 0.9
跟踪唯一标识符的数字的溢出行为应该是什么?
- 溢出时饱和。
- 溢出时环绕。
- 溢出时出错。
- 他们中的任何一个都可以。
What should be the overflow behavior of a number that tracks unique identifiers?
- Saturate on overflow.
- Wrap around on overflow.
- Error on overflow.
- Any of them is OK.
另一种常见的原始类型是string,它用于表示文本。字符串由零个或多个字符组成,这使它成为我们涵盖的第一个可以具有无限值集的原始类型。
Another common primitive type is the string, which is used to represent text. A string consists of zero or more characters, which makes it the first primitive type we are covering that can have an infinite set of values.
在早期的计算机中,每个字符都由一个字节表示,因此计算机最多有 256 个字符可用于表示文本。随着 Unicode 的标准化,旨在提供一种表示世界上所有字母表和其他字符(例如表情符号)的方法,256 个字符显然是不够的。事实上,Unicode 定义了超过一百万个字符!
In the early days of computers, each character was represented by a single byte, so computers had at most 256 characters available to represent text. With the standardization of Unicode, which aims to provide a way to represent all the world’s alphabets and other characters (such as emojis), 256 characters obviously are not enough. In fact, Unicode defines more than one million characters!
让我们以一个简单的文本断开函数为例,它接受一个字符串并将其拆分为多个给定长度的字符串,以便它可以适应文本编辑器控件的宽度,如以下代码所示。
Let’s take as an example a simple text-breaking function that takes a string and splits it into multiple strings of a given length so that it can fit within the width of a text-editor control, as shown in the following code.
函数 lineBreak(text: string, lineLength: number): string[] {
让行:string[] = []; 1个
while (text.length > lineLength) { 2
lines.push(text.substr(0, lineLength)); 3
text = text.substr(lineLength); 3个
}
lines.push(文本); 4个
返回线;
}function lineBreak(text: string, lineLength: number): string[] {
let lines: string[] = []; 1
while (text.length > lineLength) { 2
lines.push(text.substr(0, lineLength)); 3
text = text.substr(lineLength); 3
}
lines.push(text); 4
return lines;
}
乍一看,这个实现似乎是正确的。对于输入文本,例如"Testing, testing"行长为5,结果行为["Testi", "ng, t", "estin", "g"]。这是我们所期望的,因为文本每五个字符被分成多行。
At first look, this implementation seems to be correct. For input text such as "Testing, testing" and a line length of 5, the resulting lines are ["Testi", "ng, t", "estin", "g"]. This is what we expect, as the text is divided into multiple lines at every fifth character.
不过,其他符号具有更复杂的编码。举个例子,女警表情符号“ ”。尽管这看起来像一个字符,但 Java-Script 用五个字符来表示它。
回报。如果我们尝试根据它在文本中出现的位置来分解包含此表情符号的字符串,我们可能会得到意想不到的结果。如果我们尝试将文本“ ”拆分为行长,我们将取回数组。
"
".length5...
5["...
", "
"]
Other symbols have more complex encodings, though. Take, for example, “”, the woman police-officer emoji. Even though this looks like a single character, Java-Script represents it with five characters. "".length returns 5. If we try to break a string containing this emoji, depending on where it appears in the text, we can get unexpected results. If we try to break the text “...” with a line length of 5, we get back the array ["...", ""].
女警官表情符号由两个独立的表情符号组成:警官表情符号和女性标志表情符号。这两个表情符号与零宽度连接字符组合在一起"\ud002"。该字符没有图形表示;相反,它用于组合其他字符。
The woman police-officer emoji is composed of two separate emojis: the police-officer emoji and the female-sign emoji. The two emojis are combined with the zero-width joined character "\ud002". This character does not have a graphical representation; rather, it is used for combining other characters.
警察表情符号“ ”由两个相邻的字符表示,如果我们尝试将较长的字符串“ ”拆分为行长,我们可以观察到这一点。这最终分裂了警官表情符号,给了我们。是表示无法按原样打印的字符的 Unicode 转义序列。女警官表情符号,即使它被呈现为单个符号,也由五个不同的转义序列、和表示。
....
5["....\ud83d", "\udc6e
"]\uXXXX\ud83d\udc6e, \u200d, \u2640\ufe0e
The police-officer emoji, “”, is represented with two adjacent characters, as we can observe if we try to split the longer string “....” with a line length of 5. This ends up splitting the police-officer emoji, giving us ["....\ud83d", "\udc6e"]. \uXXXX are Unicode escape sequences that represent a character that cannot be printed as is. The woman police-officer emoji, even though it gets rendered as a single symbol, is represented by the five distinct escape sequences \ud83d, \udc6e, \u200d, \u2640, and \ufe0e.
在字符边界处天真地打断文本会产生无法呈现的结果,甚至会改变文本的含义。
Naïvely breaking text at character boundaries can give results that can’t be rendered and can even change the meaning of the text.
我们需要查看字符编码以更好地理解如何正确处理文本。Unicode 标准使用两个相似但不同的概念:字符和字形。字符是文本的计算机表示(警官表情符号、零宽度连接符和女性符号),而字素是用户看到的符号(女警官)。渲染文本时,我们使用字素,我们不想分解多字符字素。在编码文本时,我们使用字符。
We need to look at character encodings to better understand how to handle text properly. The Unicode standard works with two similar but distinct concepts: characters and graphemes. Characters are the computer representations of text (police-officer emoji, zero-width joiner, and female sign), and graphemes are the symbols users see (woman police officer). When rendering text, we work with graphemes, and we don’t want to break apart a multiple-character grapheme. When encoding text, we work with characters.
字形是字符的特定表示。“ C ”(粗体)和“ C ”(斜体)是字符“C”的两种不同的视觉呈现。
A glyph is a particular representation of a character. “C” (bold) and “C” (italic) are two different visual renderings of the character “C”.
字素是一个不可分割的单位,如果将其拆分成多个部分,它将失去其意义,例如女警官的例子。一个字素可以由各种字形表示。女警察的 Apple 表情符号看起来与 Microsoft 的表情符号不同;它们是呈现相同字素的不同字形(图 2.6)。
A grapheme is an indivisible unit, which would lose its meaning if it were split into components, such as the woman police-officer example. A grapheme can be represented by various glyphs. The Apple emoji for woman police officer looks different from the Microsoft one; they are different glyphs rendering the same grapheme (figure 2.6).
每个 Unicode 字符都定义为一个代码点。0x0这是一个介于和 之间的值0x10FFFF,因此有 1,114,111 个可能的代码点。这些代码点代表了世界上所有的字母表、表情符号和许多其他符号,并有足够的空间供将来添加。
Each Unicode character is defined as a code point. This is a value between 0x0 and 0x10FFFF, so there are 1,114,111 possible code points. These code points represent all the world’s alphabets, emojis, and many other symbols, with plenty of room for future additions.
对这些代码点进行编码的最直接方法是 UTF-32,每个字符使用 32 位。一个 32 位整数可以表示介于0x0和 之间的值
The most straightforward way of encoding these code points is UTF-32, which uses 32 bits for each character. A 32-bit integer can represent values between 0x0 and
0xFFFFFFFF,因此它可以容纳任何有余地的代码点。UTF-32 的问题在于它非常低效,因为它浪费了大量未使用位的空间。因此,开发了几种更紧凑的编码,这些编码对较小的代码点使用较少的位,而随着值变大,使用更多的位。这些也称为可变长度 编码。
0xFFFFFFFF, so it can fit any code point with room to spare. The problem with UTF-32 is that it’s very inefficient, as it wastes a lot of space with unused bits. Because of that, several more compact encodings were developed that use fewer bits for smaller code points and more bits as the values get larger. These are also called variable-length encodings.
最常用的编码是 UTF-16 和 UTF-8。UTF-16 是 JavaScript 使用的编码。在 UTF-16 中,单位是 16 位。适合 16 位(从0x0到0xFFFF)的代码点用单个 16 位整数表示,而需要超过 16 位(从0x10000到0x10FFFF)的代码点由两个 16 位值表示。
The most commonly used encodings are UTF-16 and UTF-8. UTF-16 is the encoding used by JavaScript. In UTF-16, the unit is 16 bits. Code points that fit in 16 bits (from 0x0 to 0xFFFF) are represented with a single 16-bit integer, whereas code points that require more than 16 bits (from 0x10000 to 0x10FFFF) are represented by two 16-bit values.
最流行的编码 UTF-8 将这种方法更进一步:单位是 8 位,代码点由一个、两个、三个或四个 8 位值表示。
UTF-8, the most popular encoding, takes this approach a step further: the unit is 8 bits and code points are represented by one, two, three, or four 8-bit values.
文本编码和操作是一个复杂的话题,整本书都专门讨论它。好消息是你不需要学习所有的细节来有效地使用字符串,但是你需要意识到它的复杂性并寻找机会来代替天真的文本操作,就像我们的文本中断示例中那样,调用封装这种复杂性的库。
Text encoding and manipulation is a complex topic, with whole books dedicated to it. The good news is that you don’t need to learn all the details to effectively work with strings, but you do need to be aware of the complexity and look for opportunities to replace naïve text manipulation, as in our text-breaking example, with calls to libraries that encapsulate this complexity.
grapheme-splitter,例如,是一个 JavaScript 文本库,可同时处理字符和字素。您可以通过运行来安装它npm install grapheme-splitter。使用grapheme-splitter,我们可以lineBreak()通过将文本拆分为字素数组然后将它们分组为字素字符串来实现在lineLength字素级别拆分文本的功能,如以下清单所示。
grapheme-splitter, for example, is a JavaScript text library that works with both characters and graphemes. You can install it by running npm install grapheme-splitter. With grapheme-splitter, we can implement the lineBreak() function to break the text at grapheme level by splitting the text into an array of graphemes and then grouping them in strings of lineLength graphemes, as the following listing shows.
导入 GraphemeSplitter = require("grapheme-splitter");
const splitter = new GraphemeSplitter();
函数 lineBreak(文本:字符串,lineLength:数字){
let graphemes: string[] = splitter.splitGraphemes(text); 1个
让行:string[] = [];
for (let i = 0; i < graphemes.length; i += lineLength) { 2
lines.push(graphemes.slice(i, i + lineLength).join("")); 2个
}
返回线;
}import GraphemeSplitter = require("grapheme-splitter");
const splitter = new GraphemeSplitter();
function lineBreak(text: string, lineLength: number) {
let graphemes: string[] = splitter.splitGraphemes(text); 1
let lines: string[] = [];
for (let i = 0; i < graphemes.length; i += lineLength) { 2
lines.push(graphemes.slice(i, i + lineLength).join("")); 2
}
return lines;
}
通过此实现,行长度为.......
5.....
[".....", "
"].
With this implementation, the strings “...” and “....” for a line length of 5 do not split the string at all, as none of the strings is larger than five graphemes, and the string “.....” correctly gets split into [".....", ""].
该grapheme-splitter库有助于防止处理字符串时出现的三种常见错误之一:
The grapheme-splitter library helps prevent one of the three common classes of errors in dealing with strings:
图 2.7显示了女警官字素是如何由两个 Unicode 字符组成的。该图还显示了它们的 UTF-16 编码和二进制表示。
Figure 2.7 shows how the woman police-officer grapheme is composed out of two Unicode characters. The figure also shows their UTF-16 encoding and binary representation.
请注意,对于相同的字素,UTF-8 编码即使最终在屏幕上具有相同的表示形式,也是不同的。UTF-8 编码是0xF0 0x9F 0x91 0xAE 0xE2 0x80 0x8D 0xE2 0x99 0x80 0xEF 0xB8 0x8F.
Note that for the same grapheme, the UTF-8 encoding, even though it ends up having the same representation on screen, is different. The UTF-8 encoding is 0xF0 0x9F 0x91 0xAE 0xE2 0x80 0x8D 0xE2 0x99 0x80 0xEF 0xB8 0x8F.
始终确保您使用正确的编码解释字节序列,并依靠字符串库在字符和字素级别操作字符串。
Always make sure you are interpreting byte sequences with the right encoding, and rely on string libraries to manipulate strings at character and grapheme levels.
- 1字节
- 2个字节
- 4字节
- 这取决于性格。
How many bytes are needed to encode a UTF-8 character?
- 1 byte
- 2 bytes
- 4 bytes
- It depends on the character.
编码一个 UTF-32 字符需要多少字节?
- 1字节
- 2个字节
- 4字节
- 这取决于性格。
How many bytes are needed to encode a UTF-32 character?
- 1 byte
- 2 bytes
- 4 bytes
- It depends on the character.
我们将讨论的最后两种常见基本类型是数组和引用。有了这些,我们就可以构建任何其他更高级的数据结构,例如列表和树。这两个原语在实现数据结构时提供了不同的权衡。我们将探索如何根据预期的访问模式(读取与写入频率)和数据密度(稀疏与密集)最好地利用它们。
The last two common primitive types we will discuss are arrays and references. With these, we can build up any of the other more advanced data structures, such as lists and trees. These two primitives offer different trade-offs in implementing data structures. We’ll explore how to best leverage them depending on expected access patterns (read versus write frequency) and data density (sparse versus dense).
固定大小的数组依次存储给定类型的多个值,从而实现高效访问。引用类型允许我们通过让组件引用其他组件来将数据结构拆分到多个位置。
Fixed-size arrays store several values of a given type one after the other, enabling efficient access. Reference types allow us to split a data structure across multiple locations by having components reference other components.
我们不会将可变大小数组视为原始类型,因为它们是使用固定大小数组和/或引用实现的,正如我们将在本节中看到的那样。
We will not consider variable-size arrays to be primitive types, because these are implemented with fixed-size arrays and/or references, as we’ll see in this section.
固定大小的数组表示连续的内存范围,其中包含多个相同类型的值。例如,一个由五个 32 位整数组成的数组是 160 位 (5 * 32) 的范围,其中前 32 位存储第一个数字,后 32 位存储下一个数字,依此类推。
Fixed-size arrays represent a contiguous range of memory that contains several values of the same type. An array of five 32-bit integers, for example, is a range of 160 bits (5 * 32) in which the first 32 bits store the first number, the second 32 bits store the next, and so on.
数组之所以是一种常见的原语而不是链表,原因在于效率:因为值是一个接一个地存储的,所以访问其中任何一个都是一项快速操作。如果一个 32 位整数数组从内存地址 101 开始,这相当于说第一个整数(在索引 0 处)存储为 101 和 132 之间的 32 位,则数组中索引 N 处的整数位于101 + N * 32 通常,如果列表从地址base开始,并且元素的大小为M ,则可以在base + N * M找到索引N处的元素. 因为内存是连续的,所以数组很有可能被分页到内存中并立即缓存,从而实现非常快速的访问。
The reason why arrays are a common primitive as opposed to, say, linked lists is efficiency: because the values are stored one after the other, accessing any one of them is a fast operation. If an array of 32-bit integers starts at memory address 101, which is the same as saying that the first integer (at index 0) is stored as the 32 bits between 101 and 132, the integer at index N in the array is at 101 + N * 32. In general, if the list starts at address base, and the size of an element is M, the element at index N can be found at base + N * M. Because the memory is contiguous, there is a high chance the array will get paged into memory and cached all at once, which enables very fast access.
相比之下,对于链表,访问第Nth 个元素需要我们从链表的头部开始,沿着next每个节点的指针,直到我们到达N第一个。无法直接计算节点的地址。节点不一定一个接一个地分配,因此内存可能必须调入和调出,直到我们到达我们想要的节点。图 2.8显示了数组和整数链表在内存中的表示。
By contrast, for a linked list, accessing the Nth element requires us to start from the head of the list and follow the next pointers of each node until we reach the Nth one. There is no way to compute the address of a node directly. Nodes are not necessarily allocated one after the other, so memory might have to be paged in and out until we reach the node we want. Figure 2.8 shows in-memory representations of an array and a linked-list of integers.
术语“固定大小”源于数组不能就地增长或收缩的事实。如果我们想让我们的数组存储六个整数而不是五个,我们将不得不分配一个可以容纳六个整数的新数组并从原始数组复制前五个。将此与链表进行对比,我们可以在链表中添加一个节点而无需修改任何现有节点。根据预期的访问模式(更多读取或更多追加),一种表示会比另一种表现更好。
The term fixed-size comes from the fact that arrays can’t be grown or shrunk in place. If we ever want to make our array store six integers instead of five, we would have to allocate a new array that can fit six integers and copy the first five over from the original array. Contrast this with a linked list, in which we can append a node without having to modify any of the existing nodes. Depending on the expected access pattern (more reads or more appends), one representation would work better than the other.
引用类型保存指向对象的指针。引用类型的值——变量的实际位——并不代表对象的内容,而是对象所在的位置。对单个对象的多个引用不会复制对象的状态,因此通过其中一个引用对对象所做的更改通过所有其他引用可见。
Reference types hold pointers to objects. The value of a reference type—the actual bits of a variable—do not represent the content of an object, but where the object can be found. Multiple references to a single object do not duplicate the state of the object, so changes made to the object through one of the references are visible through all other references.
引用类型通常用于数据结构实现中,因为它们提供了一种连接单独组件的方法,这些组件可以在运行时添加到数据结构或从数据结构中删除。
Reference types are commonly used in data structure implementations, as they provide a way to connect separate components that can be added to or removed from the data structure at run time.
在接下来的部分中,我们将了解一些常见的数据结构,以及如何使用数组、引用或两者的组合来实现它们。
In the following sections, we will look at a few common data structures and how they can be implemented with arrays, references, or a combination of the two.
许多语言都提供列表数据结构作为其库的一部分。注意这个数据结构不是原语,而是用原语实现的数据结构。随着项目的添加或删除,列表可以收缩和增长。
Many languages provide a list data structure as part of their library. Note this data structure is not a primitive, but a data structure implemented with primitives. Lists can shrink and grow as items are added or removed.
如果将列表实现为链表,我们可以添加和删除节点而无需复制任何数据,但遍历列表会很昂贵(线性时间或O(n)复杂性,其中n是列表的长度)。在清单 2.13中,NumberLinkedList是这样一个列表实现,它提供了两个函数:at(),它检索给定索引处的值,以及append(),它向列表的末尾添加一个值。该实现保留了两个引用:一个指向列表的开头,我们可以从中开始遍历,另一个指向列表的末尾,这样我们就可以在不必遍历列表的情况下追加元素。
If lists were implemented as linked lists, we could add and remove nodes without having to copy any data, but traversing the list would be expensive (linear time or O(n) complexity, where n is the length of the list). In listing 2.13, NumberLinkedList is such a list implementation that provides two functions: at(), which retrieves the value at the given index, and append(), which adds a value to the end of the list. The implementation keeps two references: one to the beginning of the list, from which we can start a traversal, and one to the end of the list, which allows us to append elements without having to traverse the list.
类 NumberListNode {
值:数字; 1
下一个:NumberListNode | 不明确的; 1个
构造函数(值:数字){
this.value = 值;
this.next = undefined;
}
}
类 NumberLinkedList {
private tail: NumberListNode = { value: 0, next: undefined }; 2
私有头:NumberListNode = this.tail; 2个
在(索引:数字):数字{
让结果:NumberListNode | undefined = this.head.next; 3
while (index > 0 && result != undefined) { 3
结果=结果.下一个;
指数 - ;
}
如果(结果 == 未定义)抛出新的 RangeError();
返回结果。值;
}
附加(值:数字){
this.tail.next = { value: value, next: undefined }; 4
this.tail = this.tail.next; 4个
}
}class NumberListNode {
value: number; 1
next: NumberListNode | undefined; 1
constructor(value: number) {
this.value = value;
this.next = undefined;
}
}
class NumberLinkedList {
private tail: NumberListNode = { value: 0, next: undefined }; 2
private head: NumberListNode = this.tail; 2
at(index: number): number {
let result: NumberListNode | undefined = this.head.next; 3
while (index > 0 && result != undefined) { 3
result = result.next;
index--;
}
if (result == undefined) throw new RangeError();
return result.value;
}
append(value: number) {
this.tail.next = { value: value, next: undefined }; 4
this.tail = this.tail.next; 4
}
}
正如我们所见,append()在这种情况下非常有效,因为它只需要将一个节点添加到尾部,然后使该新节点成为尾部。另一方面,at()要求我们从头部开始,沿着next引用移动,直到到达我们要寻找的节点。
As we can see, append() is very efficient in this case, as it only needs to add a node to the tail and then make that new node the tail. On the other hand, at() requires us to start from the head and move along next references until we reach the node we were looking for.
在下一个清单中,让我们将其与基于数组的实现进行对比,在该实现中可以高效地访问元素,但附加元素是昂贵的操作。
In the next listing, let’s contrast this with an array-based implementation, in which accessing an element can be done efficiently, but appending an element is the expensive operation.
类 NumberArrayList {
私人号码:number[] = []; 1
私有长度:number = 0; 1个
在(索引:数字):数字{
if (index >= this.length) throw new RangeError();
返回 this.numbers[index]; 2个
}
附加(值:数字){
让 newNumbers: number[] = new Array(this.length + 1); 3
for (let i = 0; i < this.length; i++) { 3
newNumbers[i] = this.numbers[i];
}
newNumbers[this.length] = 值; 4个
this.numbers = newNumbers;
这个。长度++;
}
}class NumberArrayList {
private numbers: number[] = []; 1
private length: number = 0; 1
at(index: number): number {
if (index >= this.length) throw new RangeError();
return this.numbers[index]; 2
}
append(value: number) {
let newNumbers: number[] = new Array(this.length + 1); 3
for (let i = 0; i < this.length; i++) { 3
newNumbers[i] = this.numbers[i];
}
newNumbers[this.length] = value; 4
this.numbers = newNumbers;
this.length++;
}
}
在这里,访问给定索引处的元素仅意味着在基础numbers数组中进行索引。另一方面,附加一个值成为一个复杂的操作:
Here, accessing the element at a given index simply means indexing in the underlying numbers array. On the other hand, appending a value becomes an involved operation:
每当我们需要追加一个新值时复制数组的所有元素,同样不是很有效。
Copying all the elements of the array whenever we need to append a new value is, again, not very efficient.
实际上,大多数库将列表实现为具有额外容量的数组。该数组的大小比最初需要的大,因此无需创建新数组和复制数据即可添加新元素。当数组被填满时,一个新数组被分配,元素被复制,但新数组的容量增加了一倍(图 2.9)。
In practice, most libraries implement lists as an array with extra capacity. The array has a larger size than initially needed, so new elements can be appended without having to create a new array and copy data. When the array gets filled up, a new array is allocated, and elements do get copied over, but the new array has double the capacity (figure 2.9).
使用这种试探法,数组容量呈指数级增长,因此数据不会像数组每次只增长一个元素那样被复制那么多。
With this heuristic, the array capacity grows exponentially, so data doesn’t get copied as much as it would if the array grew by only one element every time.
类 NumberList {
私人号码:number[] = new Array(1); 1个
私有长度:number = 0;
私有容量:number = 1; 1个
在(索引:数字):数字{
if (index >= this.length) throw new RangeError();
返回 this.numbers[index]; 2个
}
附加(值:数字){
如果 (this.length < this.capacity) { 3
this.numbers[length] = value; 3
这个长度++; 3
返回;
}
this.capacity = this.capacity * 2; 4
let newNumbers: number[] = new Array(this.capacity); 4个
对于(让 i = 0;i < this.length;i++){
newNumbers[i] = this.numbers[i];
}
newNumbers[this.length] = 值;
this.numbers = newNumbers;
这个。长度++;
}
}class NumberList {
private numbers: number[] = new Array(1); 1
private length: number = 0;
private capacity: number = 1; 1
at(index: number): number {
if (index >= this.length) throw new RangeError();
return this.numbers[index]; 2
}
append(value: number) {
if (this.length < this.capacity) { 3
this.numbers[length] = value; 3
this.length++; 3
return;
}
this.capacity = this.capacity * 2; 4
let newNumbers: number[] = new Array(this.capacity); 4
for (let i = 0; i < this.length; i++) {
newNumbers[i] = this.numbers[i];
}
newNumbers[this.length] = value;
this.numbers = newNumbers;
this.length++;
}
}
其他的线性数据结构,比如栈和堆,也可以用类似的方式实现。这些数据结构针对读取访问进行了优化,这始终非常高效。使用额外的容量可以使大多数写入变得高效,但有些写入在数据结构已满时需要将所有元素移动到一个新数组,这是低效的。还有内存开销,因为列表分配的元素多于正在使用的元素,以便为将来的追加腾出空间。
Other linear data structures, such as stacks and heaps, can be implemented in a similar way. These data structures are optimized for read access, which is always extremely efficient. Using the extra capacity makes most writes efficient, but some writes, when the data structure is filled to capacity, require moving all elements to a new array, which is inefficient. There is also memory overhead, as the list allocates more elements than there are in use to make room for future appends.
让我们看看另一种类型的数据结构:一种我们可以在多个地方追加项目的数据结构。这种数据结构的一个示例是二叉树,其中节点可以附加到没有两个子节点的任何节点。
Let’s look at another type of data structure: a data structure in which we can append items in multiple places. An example of such a data structure is a binary tree, in which nodes can be appended to any node that doesn’t have two children.
一种选择是将二叉树表示为数组。树的第一层,即根,最多有一个节点。树的第二层最多有两个节点:根节点的子节点。第三层最多有四个节点:前一层的孩子两个节点等等。一般来说,对于有N层次的树,一棵二叉树最多可以有1 + 2 + ... + 2 N–1 个节点,也就是2 N –1。
One option is to represent a binary tree as an array. The first level of the tree, the root, has at most one node. The second level of the tree has at most two nodes: the children of the root. The third level has at most four nodes: the children of the previous two nodes and so on. In general, for a tree with N levels, a binary tree can have at most 1 + 2 + ... + 2N–1 nodes, which is 2N–1.
我们可以通过将每个级别放在前一个级别之后来将二叉树存储在数组中。如果树不完整(并非所有级别都有所有节点),我们将缺失的节点标记为undefined。这种表示的一个优点是很容易从父节点获取其子节点:如果父节点位于 index i,则左子节点位于 index 2*i,右子节点位于 index 2*i+1。
We can store a binary tree in an array by placing each level after the previous one. If the tree is not complete (not all levels have all the nodes), we mark the missing nodes as undefined. An advantage of this representation is that it’s very easy to get from a parent to its children: if the parent is at index i, the left child node is at index 2*i, and the right child node is at index 2*i+1.
图 2.10显示了我们如何将二叉树表示为固定大小的数组。
Figure 2.10 shows how we can represent a binary tree as a fixed-size array.
只要我们不更改树中的级别数,附加节点也是有效的。但是,一旦我们增加了层级,我们不仅要复制整棵树,还需要将数组的大小加倍,以便为所有可能的新节点腾出空间,如以下清单所示。这类似于高效的列表实现。
Appending a node is also efficient as long as we don’t change the number of levels in the tree. As soon as we increase the level, though, we not only have to copy the whole tree, but also need to double the size of the array to make room for all the new possible nodes, as shown in the following listing. This is similar to the efficient list implementation.
类树 {
节点:(数字 | 未定义)[] = []; 1个
left_child_index(索引:数字):数字{
返回索引 * 2; 2个
}
right_child_index(索引:数字):数字{
返回索引 * 2 + 1; 2个
}
add_level() {
让 newNodes: (number | undefined)[] =
新数组(this.nodes.length * 2 + 1); 3个
for (let i = 0; i < this.nodes.length; i++) {
newNodes[i] = this.nodes[i]; 3个
}
this.nodes = newNodes;
}
}class Tree {
nodes: (number | undefined)[] = []; 1
left_child_index(index: number): number {
return index * 2; 2
}
right_child_index(index: number): number {
return index * 2 + 1; 2
}
add_level() {
let newNodes: (number | undefined)[] =
new Array(this.nodes.length * 2 + 1); 3
for (let i = 0; i < this.nodes.length; i++) {
newNodes[i] = this.nodes[i]; 3
}
this.nodes = newNodes;
}
}
这种实现的缺点是,如果树是稀疏的,所需的额外空间量可能是不可接受的(图 2.11)。
The drawback of this implementation is that the amount of additional space required can be unacceptable if the tree is sparse (figure 2.11).
由于额外的空间开销,二叉树通常使用引用以更紧凑的表示形式表示。一个节点存储一个值和对其子节点的引用。
Because of the extra-space overhead, binary trees are usually represented with a more compact representation using references. A node stores a value and references to its children.
树节点类 {
值:数字; 1
左:TreeNode | 不明确的; 2
右:TreeNode | 不明确的; 2个
构造函数(值:数字){
this.value = 值;
this.left = undefined;
this.right = undefined;
}
}class TreeNode {
value: number; 1
left: TreeNode | undefined; 2
right: TreeNode | undefined; 2
constructor(value: number) {
this.value = value;
this.left = undefined;
this.right = undefined;
}
}
通过此实现,树由对其根节点的引用表示。从那里,在左右孩子之后,我们可以访问树中的任何节点。在任何地方附加一个节点只需要分配一个新节点并设置其父节点的leftor属性。图 2.12显示了我们如何使用引用来表示稀疏树。 right
With this implementation, a tree is represented by a reference to its root node. From there, following left and right children, we can access any node in the tree. Appending a node anywhere involves just allocating a new node and setting the left or right property of its parent. Figure 2.12 shows how we can represent a sparse tree using references.
尽管引用本身需要一些非零内存来表示,但所需的空间量与节点数成正比。对于稀疏树,这比基于数组的实现要好得多,其中空间随着层数呈指数增长。
Although the references themselves require some nonzero memory to represent, the amount of space required is proportional to the number of nodes. For sparse trees, this is much better than the array-based implementation, in which space grows exponentially with the number of levels.
一般来说,元素可以添加到多个地方的稀疏数据结构,我们希望有很多“差距”,通过让元素引用其他元素更好地表示,而不是将整个数据结构放在一个固定大小的数组中这最终会产生不可接受的开销。
In general, sparse data structures where elements can be added in multiple places and we expect to have a lot of “gaps” are better represented by having elements refer to other elements, as opposed to placing the whole data structure in a fixed-size array that would end up having unacceptable overhead.
一些编程语言提供其他类型的数据结构作为原语,并具有内置语法支持。一种常见的此类类型是关联数组,也称为字典或哈希表。这种类型的数据结构表示一组键值对,其中,给定一个键,可以有效地检索值。
Some programming languages provide other types of data structures as primitives, with built-in syntax support. A common such type is the associative array, also known as dictionary or hash table. This type of data structure represents a set of key-value pairs where, given a key, the value can be retrieved efficiently.
尽管您在遵循前面的代码示例时可能会想到什么,但 Java-Script/TypeScript 数组是关联数组。这些语言不提供固定大小的数组原始类型。代码示例展示了如何在固定大小的数组上实现数据结构。固定大小的数组假定非常有效的索引和不可变的大小。在 JavaScript/TypeScript 中并非如此。我们查看固定大小数组而不是关联数组的原因是关联数组数据结构可以用数组和引用来实现。出于说明目的,我们将 TypeScript 数组视为固定大小,因此代码示例可以直接翻译成大多数其他流行的编程语言。
Despite what you may have thought as you followed the previous code examples, Java-Script/TypeScript arrays are associative arrays. The languages do not provide a fixed-size array primitive type. The code examples show how data structures can be implemented over fixed-size arrays. A fixed-size array assumes extremely efficient indexing and immutable size. This is not really the case in JavaScript/TypeScript. The reason we looked at fixed-size arrays instead of associative arrays is that an associative array data structure can be implemented with arrays and references. For illustrative purposes, we treated TypeScript arrays as fixed-size, so the code samples can be directly translated into most other popular programming languages.
Java 和 C# 等语言提供字典或哈希映射作为其库的一部分,而数组和引用是基本类型。JavaScript 和 Python 提供关联数组作为原始类型,但它们的运行时也使用数组和引用来实现它们。数组和引用是表示特定内存布局和访问模型的较低级别的构造,而关联数组是较高级别的抽象。
Languages such as Java and C# provide dictionaries or hash maps as part of their library, whereas arrays and references are primitives. JavaScript and Python provide associative arrays as primitive types, but their run times also implement them with arrays and references. Arrays and references are lower-level constructs that represent certain memory layouts and access models, whereas associative arrays are higher-level abstractions.
关联数组通常实现为固定大小的列表数组。哈希函数采用任意类型的键并返回固定大小数组的索引。键值对被添加到数组中给定索引处的列表或从列表中检索。使用该列表是因为多个键可以散列到同一个索引(图 2.13)。
An associative array is often implemented as a fixed-size array of lists. A hash function takes a key of an arbitrary type and returns an index to the fixed-size array. The key-value pair is added to or retrieved from the list at the given index in the array. The list is used because multiple keys can hash to the same index (figure 2.13).
通过键查找值涉及找到键值对所在的列表,遍历它直到找到键,然后返回值。如果列表变得太长,查找时间会增加,因此高效的关联数组实现通过增加数组的大小来重新平衡,从而使列表更小。
Looking up a value by key involves finding the list where the key-value pair sits, traversing it until the key is found, and returning the value. If lists become too long, lookup time increases, so efficient associative array implementations rebalance by increasing the size of the array, thus making the lists smaller.
一个好的散列函数可以确保键通常在列表中均匀分布,从而使列表的长度相似。
A good hashing function ensures that keys usually get distributed across the lists evenly so that the lists are similar in length.
在上一节中,我们了解了数组和引用如何足以实现其他数据结构。根据预期的访问模式(例如读取与写入频率)和数据的预期形状(密集与稀疏),我们可以选择正确的基元来表示数据结构的组件并将它们组合起来以获得最有效的实现。
In the preceding section, we saw how arrays and references are enough to implement other data structures. Depending on the expected access patterns (such as read versus write frequency) and expected shape of the data (dense versus sparse), we can pick the right primitives to represent components of the data structure and combine them to get the most efficient implementation.
固定大小的数组具有极快的读取/更新能力,可以轻松表示密集数据。对于可变大小的数据结构,引用在追加时表现更好,并且可以更轻松地表示稀疏数据。
Fixed-size arrays have extremely fast read/update capabilities and can easily represent dense data. For variable-size data structures, references perform better on append and can represent sparse data more easily.
哪种数据结构最适合以随机顺序访问其元素?
- 链表
- 大批
- 字典
- 队列
Which data structure is best suited for accessing its elements in random order?
- Linked list
- Array
- Dictionary
- Queue
c—函数不返回任何有意义的东西,所以void单位类型是一个很好的返回类型。
c—The function doesn’t return anything meaningful, so the void unit type is a good return type.
a——函数永远不会返回,所以空类型never是一个很好的返回类型。
a—The function never returns, so the empty type never is a good return type.
b—计数器只增加一次,因为函数返回false,所以布尔表达式被短路。
b—The counter is incremented only once because the function returns false, so the Boolean expression is short-circuited.
c—false由于浮点数舍入,表达式的计算结果为。
c—The expression evaluates to false because of float rounding.
c—因为标识符需要是唯一的,所以出错是首选行为。
c—Because identifiers need to be unique, erroring out is the preferred behavior.
d—UTF-8 是可变长度编码。
d—UTF-8 is a variable-length encoding.
c—UTF-32为定长编码;所有字符都以四个字节编码。
c—UTF-32 is a fixed-length encoding; all characters are encoded in four bytes.
b—数组最适合随机访问。
b—Arrays are best suited for random access.
本章涵盖
This chapter covers
在第 2 章中,我们研究了一些构成类型系统构建块的常见原始类型。在本章中,我们将研究将它们组合起来定义新类型的方法。
In chapter 2, we looked at some common primitive types that form the building blocks of a type system. In this chapter, we’ll look at ways to combine them to define new types.
我们将介绍复合类型,它将几种类型的值组合在一起。我们将研究命名成员如何赋予数据意义并降低误解的可能性,以及我们如何确保值在需要满足特定约束时格式正确。
We’ll cover compound types, which group values of several types. We’ll look at how naming members gives meaning to data and lowers the chance of misinterpretation, and how we can ensure that values are well-formed when they need to meet certain constraints.
接下来,我们将讨论 either-or 类型,它包含来自多种类型之一的单个值。我们将研究一些常见类型,例如可选类型、任一种类型和变体,以及这些类型的一些应用。例如,我们将看到返回结果或错误通常比返回结果和错误更安全。
Next, we’ll go over either-or types, which contain a single value from one of several types. We will look at some common types such as optional types, either types, and variants, as well as a few applications of these types. We’ll see, for example, how returning a result or an error is usually safer than returning a result and an error.
作为非此即彼类型的应用,我们将研究访问者设计模式,并将利用类层次结构的实现与使用变体来存储和操作对象的实现进行对比。
As an application of either-or types, we’ll take a look at the visitor design pattern and contrast an implementation that leverages class-hierarchies with an implementation that uses a variant to store and operate on objects.
最后,我们将提供代数数据类型 (ADT) 的描述,并了解它们与本章讨论的主题有何关联。
Finally, we’ll provide a description of algebraic data types (ADTs) and see how they relate to the topics discussed in this chapter.
组合类型最明显的方法是将它们分组以形成新类型。让我们在平面上取一对 X 和 Y 坐标。X 和 Y 坐标都具有类型number。平面上的一个点同时具有 X 和 Y 坐标,因此它将这两种类型组合成一种新类型,其中值是数字对。
The most obvious way to combine types is to group them to form new types. Let’s take a pair of X and Y coordinates on a plane. Both X and Y coordinates have the type number. A point on the plane has both an X and a Y coordinate, so it combines the two types into a new type in which values are pairs of numbers.
通常,以这种方式组合一种或多种类型会为我们提供一种新类型,其中的值是组件类型的所有可能组合(图 3.1)。
In general, combining one or more types this way gives us a new type in which the values are all the possible combinations of the component types (figure 3.1).
请注意,我们谈论的是组合类型的值,而不是它们的操作。当我们在第 8 章中查看面向对象编程的元素时,我们将看到操作是如何组合的。现在,我们将坚持价值观。
Note that we’re talking about combining values of the types, not their operations. We’ll see how operations combine when we look at elements of object-oriented programming in chapter 8. For now, we’ll stick to values.
假设我们要计算定义为坐标对的两点之间的距离。我们可以定义一个函数,获取第一个点的 X 坐标和 Y 坐标,以及第二个点的 X 坐标和 Y 坐标,然后计算两者之间的距离,如以下清单所示。
Let’s say we want to compute the distance between two points defined as pairs of coordinates. We can define a function that takes the X coordinate and Y coordinate of the first point, and the X coordinate and the Y coordinate of the second point, and then computes the distance between the two, as shown in the following listing.
函数距离(x1:数字,y1:数字,x2:数字,y2:数字)
: 数字 {
返回 Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}function distance(x1: number, y1: number, x2: number, y2: number)
: number {
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}
这行得通,但并不理想:如果我们处理的是点,则x1没有相应的 Y 坐标是没有意义的。我们的应用程序可能需要在多个地方操作点,因此我们可以将它们分组在一个元组中,而不是传递独立的 X 和 Y 坐标。
This works, but it’s not ideal: if we are dealing with points, x1 is meaningless without the corresponding Y coordinate. Our application likely needs to manipulate points in multiple places, so instead of passing around independent X and Y coordinates, we could group them in a tuple.
元组类型由一组组件类型组成,我们可以通过它们在元组中的位置来访问它们。元组提供了一种以特殊方式对数据进行分组的方法,允许我们将多个不同类型的值作为单个变量传递。
Tuple types consist of a set of component types, which we can access by their position in the tuple. Tuples provide a way to group data in an ad hoc way, allowing us to pass around several values of different types as a single variable.
使用元组,我们可以将成对的 X 和 Y 坐标一起作为点传递。这使得代码更易于阅读和编写。它更容易阅读,因为现在很清楚我们正在处理点,而且它更容易编写,因为我们可以简单地使用point: Pointinstead of x: number, y: number,如下一个清单所示。
Using tuples, we can pass around pairs of X and Y coordinates together as points. This makes the code both easier to read and easier to write. It’s easier to read as it is now clear that we are dealing with points, and it’s easier to write as we can simply use point: Point instead of x: number, y: number, as shown in the next listing.
输入 Point = [number, number]; 1个
函数距离(点 1:点,点 2:点):数字 {
返回 Math.sqrt(
( point1[0] - point2[0] ) ** 2 + ( point1[1] - point2[1] ) ** 2);
}type Point = [number, number]; 1
function distance(point1: Point, point2: Point): number {
return Math.sqrt(
(point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2);
}
当我们需要从一个函数返回多个值时,元组也很有用,如果没有对值进行分组的方法,我们就不能轻易地做到这一点。另一种方法是使用out参数,由函数更新的参数,但这会使代码更难理解。
Tuples are also useful when we need to return multiple values from a function, which we can’t easily do without a way to group values. The alternative is to use out parameters, arguments that are updated by the function, but that makes the code harder to follow.
大多数语言都将元组作为内置语法或作为其库的一部分提供,但让我们看看如果元组不可用,我们将如何实现它。在下面的代码中,我们将实现一个具有两种组件类型的通用元组,也称为pair。
Most languages offer tuples as built-in syntax or as part of their library, but let’s look at how we would implement a tuple if it were unavailable. In the following code we’ll implement a generic tuple with two component types, also known as a pair.
类对 <T1, T2> {
m0: T1; 1
立方米:T2; 1个
构造函数(m0:T1,m1:T2){
这个.m0 = m0;
这个.m1 = m1;
}
}
输入 Point = Pair<number, number> ;
函数距离(点 1:点,点 2:点):数字 {
返回 Math.sqrt(
( point1.m0 - point2.m0 ) ** 2 + ( point1.m1 - point2.m1 ) ** 2);
}class Pair<T1, T2> {
m0: T1; 1
m1: T2; 1
constructor(m0: T1, m1: T2) {
this.m0 = m0;
this.m1 = m1;
}
}
type Point = Pair<number, number>;
function distance(point1: Point, point2: Point): number {
return Math.sqrt(
(point1.m0 - point2.m0) ** 2 + (point1.m1 - point2.m1) ** 2);
}
将类型视为可能值的集合,如果 X 坐标可以是定义的集合中的任何值number,并且类似地,Y 坐标可以是定义的集合中的任何值number,则Point元组可以是定义为集合中的任何值对<number, number>。
Looking at types as sets of possible values, if the X coordinate can be any value in the set defined by number and, similarly, the Y coordinate can be any value in the set defined by number, the Point tuple can be any value in the set defined as the pair <number, number>.
将点定义为数字对是可行的,但我们失去了一些意义:我们可以将一对数字解释为 X 和 Y 坐标或 Y 和 X 坐标(图 3.2)。
Defining points as pairs of numbers works, but we lose some meaning: we can interpret a pair of numbers as either X and Y coordinates or Y and X coordinates (figure 3.2).
到目前为止,在我们的示例中,我们假设第一个分量是 X 坐标,第二个分量是 Y 坐标。这行得通,但有出错的余地。如果我们可以在类型系统中对含义进行编码,并确保没有空间将 X 误解为 Y 或将 Y 误解为 X,那就更好了。我们可以通过使用记录类型来做到这一点。
In our examples so far, we assumed that the first component is the X coordinate and the second the Y coordinate. This works but leaves room for error. It is better if we can encode the meaning within the type system and ensure that there is no room to misinterpret X as Y or Y as X. We can do this by using a record type.
记录类型,类似于元组,组合了多种其他类型。记录类型允许我们给它们的组件名称并通过名称访问它们,而不是通过它们在元组中的位置访问组件值。记录类型被称为record或struct使用不同的语言。
Record types, similar to tuples, combine multiple other types. Instead of the component values being accessed by their position in the tuple, record types allow us to give their components names and access them by name. Record types are known as record or struct in different languages.
如果我们将 our 定义Point为记录,我们可以将名称x和分配y给两个组件,并且不会留下歧义的余地,如下一个清单所示。
If we define our Point as a record, we can assign the names x and y to the two components and leave no room for ambiguity, as the next listing shows.
类点{
x:数字; 1
y:数字; 1个
构造函数(x:数字,y:数字){
这个.x = x;
这个.y = y;
}
}
函数距离(点 1:点,点 2:点):数字 {
返回 Math.sqrt(
( point1.x - point2.x ) ** 2 + ( point1.y - point2.y ) ** 2);
}class Point {
x: number; 1
y: number; 1
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function distance(point1: Point, point2: Point): number {
return Math.sqrt(
(point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2);
}
根据经验,通常最好使用命名组件定义记录,而不是四处传递元组。元组没有命名它们的组件这一事实为误解留下了空间。元组在效率或功能方面并没有真正提供比记录更好的东西,除了我们通常可以在我们使用它们的地方内联声明它们,而我们通常必须为记录提供单独的定义。在大多数情况下,单独的定义值得添加,因为它为我们的变量提供了额外的含义。
As a rule of thumb, it’s usually best to define records with named components instead of passing tuples around. The fact that tuples do not name their components leaves room for misinterpretation. Tuples don’t really provide anything better than records in terms of efficiency or functionality, except that we can usually declare them inline where we are using them, whereas we usually have to provide a separate definition for records. In most cases, the separate definition is worth adding, as it provides extra meaning to our variables.
在记录类型可以有关联方法的语言中,通常有一种方法来定义其成员的可见性。成员可以定义为public(可从任何地方访问)、private(仅可从记录内访问)等。在 TypeScript 中,成员默认是公共的。
In languages in which record types can have associated methods, there is usually a way to define the visibility of their members. A member can be defined as public (accessible from anywhere), private (accessible only from within the record), and so on. In TypeScript, members are public by default.
一般来说,当我们定义记录类型时,如果成员是独立的并且可以变化而不会引起问题,那么将它们标记为公共就可以了。这是定义为成对的 X 和 Y 坐标的点的情况:当点在平面上移动时,其中一个坐标可以独立于另一个坐标而变化。
In general, when we define record types, if the members are independent and can vary without causing issues, it’s fine to mark them as public. This is the case with points defined as pairs of X and Y coordinates: one of the coordinates can change independently of the other coordinate as a point moves on the plane.
让我们举另一个例子,其中成员不能独立变化而不会引起问题:我们在第 2 章中看到的货币类型,由dollar数量和cents数量组成。让我们使用以下定义格式正确的货币金额的规则来增强类型的定义:
Let’s take another example in which the members can’t vary independently without causing issues: the currency type we looked at in chapter 2, formed by a dollar amount and a cents amount. Let’s enhance the definition of the type with the following rules that define a well-formed currency amount:
此类确保值格式正确的规则也称为不变量,因为即使构成复合类型的值发生变化,它们也不应发生变化。如果我们公开成员,外部代码可以更改它们,我们最终可能会得到格式错误的记录,如下一个清单所示。
Such rules that ensure a value is well-formed are also called invariants, as they shouldn’t change even as the values that make up the composite type change. If we make the members public, external code can change them, and we can end up with ill-formed records, as shown in the next listing.
类货币{
美元:数字;
分:数量;
构造函数(美元:数字,美分:数字){
如果 (!Number.isSafeInteger(cents) || cents < 0) 1
抛出新的错误();
美元 = 美元 + Math.floor(cents / 100); 2
美分 = 美分 % 100; 2个
如果 (!Number.isSafeInteger(dollars) || dollars < 0) 1
抛出新的错误();
this.dollars = 美元;
this.cents = 美分;
}
}
让金额:货币=新货币(5、50);
金额.cents = 300; 3个class Currency {
dollars: number;
cents: number;
constructor(dollars: number, cents: number) {
if (!Number.isSafeInteger(cents) || cents < 0) 1
throw new Error();
dollars = dollars + Math.floor(cents / 100); 2
cents = cents % 100; 2
if (!Number.isSafeInteger(dollars) || dollars < 0) 1
throw new Error();
this.dollars = dollars;
this.cents = cents;
}
}
let amount: Currency = new Currency(5, 50);
amount.cents = 300; 3
通过将成员设为私有并提供更新它们的方法以确保保持不变量,可以防止这种情况,如以下清单所示。如果我们处理所有不变量无效的情况,我们可以确保对象始终处于有效状态,因为更改它会为我们提供另一个格式正确的对象或导致异常。
This situation can be prevented by making the members private and providing methods to update them that ensure the invariants are maintained, as shown in the following listing. If we handle all cases in which invariants would be invalidated, we can ensure that an object is always in a valid state, as changing it would give us another well-formed object or result in an exception.
类货币{
私人美元:数量= 0; 1个
私分:数量=0; 1个
构造函数(美元:数字,美分:数字){
this.assignDollars(美元);
this.assignCents(美分);
}
getDollars(): 数字 {
返回 this.dollars;
}
assignDollars(美元:数字){
如果 (!Number.isSafeInteger(dollars) || dollars < 0) 2
抛出新的错误();
this.dollars = 美元;
}
getCents(): 数字 {
返回this.cents;
}
assignCents(美分:数字){
如果 (!Number.isSafeInteger(cents) || cents < 0) 2
抛出新的错误();
this.assignDollars(this.dollars + Math.floor(cents / 100)); 3个
this.cents = 美分 % 100;
}
}class Currency {
private dollars: number = 0; 1
private cents: number = 0; 1
constructor(dollars: number, cents: number) {
this.assignDollars(dollars);
this.assignCents(cents);
}
getDollars(): number {
return this.dollars;
}
assignDollars(dollars: number) {
if (!Number.isSafeInteger(dollars) || dollars < 0) 2
throw new Error();
this.dollars = dollars;
}
getCents(): number {
return this.cents;
}
assignCents(cents: number) {
if (!Number.isSafeInteger(cents) || cents < 0) 2
throw new Error();
this.assignDollars(this.dollars + Math.floor(cents / 100)); 3
this.cents = cents % 100;
}
}
外部代码现在必须通过assignDollars()和assignCents()函数,以确保维护所有不变量:如果提供的值无效,则会抛出异常。如果美分数大于 100,则转换为美元。
External code now has to go through the assignDollars() and assignCents() functions, which ensure that all invariants are maintained: if the provided values are invalid, exceptions are thrown. If the number of cents is larger than 100, it is converted to dollars.
一般来说,如果没有要强制执行的不变量,例如平面上一个点的独立 X 和 Y 分量,我们应该可以直接访问记录的公共成员。另一方面,如果我们有一组规则来定义记录的格式正确意味着什么,我们应该使用私有成员和方法来更新它们以确保规则得到执行。
In general, we should be fine providing direct access to public members of a record if there are no invariants to be enforced, such as the independent X and Y components of a point on a plane. On the other hand, if we have a set of rules that define what it means for a record to be well-formed, we should use private members and methods to update them to ensure that the rules are enforced.
另一种选择是使成员不可变,如以下清单所示,在这种情况下,我们可以在初始化期间确保记录格式正确,但随后我们可以允许直接访问成员,因为它们不能被更改外部代码。
Another option is to make the members immutable, as shown in the following listing, in which case we can ensure during initialization that the record is well-formed, but then we can allow direct access to the members because they can’t be changed by external code.
类货币{
只读美元:数字; 1
只读分:数字; 1个
构造函数(美元:数字,美分:数字){
如果 (!Number.isSafeInteger(cents) || cents < 0) 2
抛出新的错误();
美元 = 美元 + Math.floor(cents / 100); 2个
美分 = 美分 % 100;
如果 (!Number.isSafeInteger(dollars) || dollars < 0) 2
抛出新的错误();
this.dollars = 美元;
this.cents = 美分;
}
}class Currency {
readonly dollars: number; 1
readonly cents: number; 1
constructor(dollars: number, cents: number) {
if (!Number.isSafeInteger(cents) || cents < 0) 2
throw new Error();
dollars = dollars + Math.floor(cents / 100); 2
cents = cents % 100;
if (!Number.isSafeInteger(dollars) || dollars < 0) 2
throw new Error();
this.dollars = dollars;
this.cents = cents;
}
}
如果成员是不可变的,我们就不再需要它们的函数来维护不变量。成员设置的唯一时间是在构造期间,因此我们可以将所有验证逻辑移到那里。不可变数据还有其他优点:从不同线程并发访问此数据是安全的,因为数据无法更改。当一个线程修改一个值而另一个线程正在使用它时,可变性会导致数据竞争。
If the members are immutable, we no longer need functions for them to uphold the invariants. The only time when the members are set is during construction, so we can move all the validation logic there. Immutable data has other advantages: accessing this data concurrently from different threads is guaranteed to be safe, as the data can’t change. Mutability can cause data races, when one thread modifies a value while another thread is using it.
具有不可变成员的记录的缺点是我们需要在需要新值时创建一个新实例。根据创建新实例的成本,我们可能会选择可以使用 getter 和 setter 方法就地更新成员的记录,或者我们可能会选择每次更新都需要创建新对象的实现。
The drawback of records with immutable members is that we need to create a new instance whenever we want a new value. Depending on how expensive it is to create new instances, we might opt for a record in which the members can be updated in place by using getter and setter methods, or we might go with an implementation in which each update requires creating a new object.
目标是防止外部代码进行绕过验证规则的更改,方法是将成员设为私有并通过方法路由所有访问,或者将成员设为不可变并在构造函数中应用验证。
The goal is to prevent external code from making changes that bypass our validation rules, either by making members private and routing all access through methods or by making the members immutable and applying validation in the constructor.
在 3D 空间中定义点的首选方法是什么?
- type Point = [number, number, number];
- type Point = number | number | number;
- type Point = { x: number, y: number, z: number };
- type Point = any;
What is the preferred way of defining a point in 3D space?
- type Point = [number, number, number];
- type Point = number | number | number;
- type Point = { x: number, y: number, z: number };
- type Point = any;
到目前为止,我们已经了解了通过对类型进行分组来组合类型,这样值就由来自每个成员类型的一个值组成。我们可以组合类型的另一种基本方法是非此即彼,其中值是一个或多个基础类型的一组可能值中的任何一个(图 3.3 )。
So far, we’ve looked at combining types by grouping them such that values are composed of one value from each of the member types. Another fundamental way in which we can combine types is either-or, in which a value is any one of a possible set of values of one or more underlying types (figure 3.3).
让我们从一个非常简单的任务开始:在类型系统中编码星期几。我们可以说星期几是 0 到 6 之间的数字,0 是一周的第一天,6 是最后一天。这不太理想,因为编写代码的多个工程师可能对一周的第一天有不同的看法。美国、加拿大和日本等国家将星期日视为一周的第一天,而 ISO 8601 标准和大多数欧洲国家将星期一视为一周的第一天。
Let’s start with a very simple task: encoding a day of the week in the type systems. We could say the day of the week is a number between 0 and 6, 0 being the first day of the week and 6 being the last one. This is less than ideal, because multiple engineers working on the code might have different opinions of what the first day of the week is. Countries such as the United States, Canada, and Japan consider Sunday to be the first day of the week, whereas the ISO 8601 standard and most European countries consider Monday to be the first day of the week.
函数 isWeekend(dayOfWeek: number): 布尔值 {
返回 dayOfWeek == 5 || 星期几 == 6; 1个
}
函数 isWeekday(dayOfWeek: number): 布尔值 {
返回 dayOfWeek >= 1 && dayOfWeek <= 5; 2
}function isWeekend(dayOfWeek: number): boolean {
return dayOfWeek == 5 || dayOfWeek == 6; 1
}
function isWeekday(dayOfWeek: number): boolean {
return dayOfWeek >= 1 && dayOfWeek <= 5; 2
}
从这个代码示例中可以明显看出,这两个函数不可能都正确。如果 0 代表星期天,isWeekend()则不正确;如果 0 代表星期一,isWeekday()则不正确。不幸的是,因为 0 的含义不是强制的,而是约定俗成的,所以没有自动的方法来防止这个错误。
It should be obvious from this code example that the two functions can’t both be correct. If 0 represents Sunday, isWeekend() is incorrect; if 0 represents Monday, isWeekday() is incorrect. Unfortunately, because the meaning of 0 is not enforced but determined by convention, there is no automatic way to prevent this error.
另一种方法是声明一组常量值来表示星期几,并确保在需要一周中的某一天时使用这些常量。
An alternative is to declare a set of constant values to represent the days of the week and make sure that these constants are used whenever a day of the week is expected.
常量星期日:数字= 0;
const 星期一:数字 = 1;
const 星期二:数字 = 2;
const 星期三:数字 = 3;
const 星期四:数字 = 4;
const 星期五:数字 = 5;
const 星期六:数字 = 6;
函数 isWeekend(dayOfWeek: number): 布尔值 {
返回 dayOfWeek ==星期六|| dayOfWeek ==周日; 1个
}
函数 isWeekday(dayOfWeek: number): 布尔值 {
返回 dayOfWeek >=星期一&& dayOfWeek <=星期五; 1
}const Sunday: number = 0;
const Monday: number = 1;
const Tuesday: number = 2;
const Wednesday: number = 3;
const Thursday: number = 4;
const Friday: number = 5;
const Saturday: number = 6;
function isWeekend(dayOfWeek: number): boolean {
return dayOfWeek == Saturday || dayOfWeek == Sunday; 1
}
function isWeekday(dayOfWeek: number): boolean {
return dayOfWeek >= Monday && dayOfWeek <= Friday; 1
}
这个实现比之前的实现稍微好一点,但仍然存在一个问题:看函数的声明,不清楚 type 的参数的期望值是什么number。刚接触代码的人怎么知道每当他们看到 a 时dayOfWeek: number,他们应该使用其中一个常量?他们可能不知道这些常量存在于某个模块中的某处,相反,他们可以自己解释这些数字,如我们在清单 3.8中的第一个示例。也有人可以使用完全无效的值调用该函数,例如-1或10。一个更好的解决方案是声明一个星期几的枚举。
This implementation is slightly better than the previous implementation, but there’s still a problem: looking at the declaration of a function, it’s not clear what the expected values are for an argument of type number. How is someone who’s new to the code supposed to know that whenever they see a dayOfWeek: number, they should use one of the constants? They may not be aware that these constants exist somewhere in some module, and instead, they could interpret the number themselves, as in our first example in listing 3.8. Someone can also call the function with completely invalid values, such as -1 or 10. An even better solution is to declare an enumeration for the days of the week.
枚举 DayOfWeek { 1
周日、
周一、
周二、
周三、
周四、
周五、
周六
}
函数 isWeekend(dayOfWeek: DayOfWeek ): boolean { 2
返回 dayOfWeek == DayOfWeek.Saturday
|| dayOfWeek == DayOfWeek.Sunday;
}
函数 isWeekday(dayOfWeek: DayOfWeek ): boolean { 2
返回 dayOfWeek >= DayOfWeek.Monday
&& dayOfWeek <= DayOfWeek.Friday;
}enum DayOfWeek { 1
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
function isWeekend(dayOfWeek: DayOfWeek): boolean { 2
return dayOfWeek == DayOfWeek.Saturday
|| dayOfWeek == DayOfWeek.Sunday;
}
function isWeekday(dayOfWeek: DayOfWeek): boolean { 2
return dayOfWeek >= DayOfWeek.Monday
&& dayOfWeek <= DayOfWeek.Friday;
}
通过这种方法,我们直接将星期几编码为一个枚举,这有两大优势:星期一和星期日没有歧义,因为它们在代码中拼写出来了。此外,很明显,在查看需要参数的函数声明时dayOfWeek: DayOfWeek,我们应该传入 的成员DayOfWeek,例如DayOfWeek.Tuesday,而不是数字。
With this approach, we directly encode the days of the week in an enumeration that has two big advantages: there is no ambiguity about what is Monday and what is Sunday, as they are spelled out in the code. Also, it’s very clear, when looking at a function declaration expecting a dayOfWeek: DayOfWeek argument, that we should pass in a member of DayOfWeek, such as DayOfWeek.Tuesday, not a number.
这是将一组值组合成新类型的基本示例。该类型的变量可以是提供的值之一。每当我们有一小组可能的值并希望以明确的方式表示它们时,我们就会使用枚举。接下来,让我们看看如何将这个概念应用于类型而不是值。
This is a basic example of combining a set of values into a new type. A variable of that type can be one of the provided values. We would use enumerations whenever we have a small set of possible values and want to represent them in an unambiguous manner. Next, let’s see how we apply this concept to types instead of values.
假设我们要将string作为用户输入提供的 转换为DayOfWeek. 如果我们可以将字符串解释为星期几,我们想返回一个DayOfWeek值,但如果我们不能解释它,我们想明确地说星期几是undefined。我们可以使用|类型运算符在 TypeScript 中实现这一点,它允许我们组合类型,如以下代码所示。
Let’s say we want to convert a string, provided as user input, to a DayOfWeek. If we can interpret the string as a day of week, we want to return a DayOfWeek value, but if we can’t interpret it, we want to explicitly say that the day of the week is undefined. We can implement this in TypeScript by using the | type operator, which allows us to combine types, as shown in the following code.
函数 parseDayOfWeek(input: string): DayOfWeek | 未定义{ 1
开关(输入。toLowerCase()){
case "星期天": 返回 DayOfWeek.Sunday;
case "monday": 返回 DayOfWeek.Monday;
case "tuesday": 返回 DayOfWeek.Tuesday;
case "wednesday": 返回 DayOfWeek.Wednesday;
case "thursday": 返回 DayOfWeek.Thursday;
case "friday": 返回 DayOfWeek.Friday;
案例“星期六”:返回DayOfWeek.Saturday;
默认值:返回未定义; 2个
}
}
函数使用输入(输入:字符串){
让结果:DayOfWeek | undefined = parseDayOfWeek(输入);
如果(结果 === 未定义){ 3
console.log(`无法解析“${input}”`);
} 别的 {
让 dayOfWeek: DayOfWeek = 结果; 4个
/* 使用星期几 */
}
}function parseDayOfWeek(input: string): DayOfWeek | undefined { 1
switch (input.toLowerCase()) {
case "sunday": return DayOfWeek.Sunday;
case "monday": return DayOfWeek.Monday;
case "tuesday": return DayOfWeek.Tuesday;
case "wednesday": return DayOfWeek.Wednesday;
case "thursday": return DayOfWeek.Thursday;
case "friday": return DayOfWeek.Friday;
case "saturday": return DayOfWeek.Saturday;
default: return undefined; 2
}
}
function useInput(input: string) {
let result: DayOfWeek | undefined = parseDayOfWeek(input);
if (result === undefined) { 3
console.log(`Failed to parse "${input}"`);
} else {
let dayOfWeek: DayOfWeek = result; 4
/* Use dayOfWeek */
}
}
此parseDayOfWeek()函数返回一个DayOfWeek或undefined。该use-Input()函数调用此函数,然后尝试解包结果、记录错误或以DayOfWeek它可以使用的值结束。
This parseDayOfWeek() function returns a DayOfWeek or undefined. The use-Input() function calls this function and then tries to unwrap the result, logging an error or ending up with a DayOfWeek value that it can use.
可选类型,也称为可能类型,表示另一种类型的可选值T。可选类型的实例可以包含 type 的值(任何值)T或指示不存在 type 值的特殊值T。
An optional type, also known as a maybe type, represents an optional value of another type T. An instance of the optional type can hold a value (any value) of type T or a special value indicating the absence of a value of type T.
一些主流编程语言不支持以这种方式组合类型的语法级支持,但一组通用结构可作为库使用。我们的DayOfWeekorundefined示例是可选类型。可选值要么包含其基础类型的值,要么不包含任何值。
Some mainstream programming languages do not have syntax-level support for combining types this way, but a set of common constructs is available as libraries. Our DayOfWeek or undefined example is an optional type. An optional contains either a value of its underlying type or no value.
可选类型通常包装另一种类型作为泛型类型参数提供,并提供几个方法:一个hasValue()方法,它告诉我们是否有一个实际值,以及一个getValue(),它返回该值。在未设置任何值时尝试调用getValue()会导致抛出异常,如下一个清单所示。
An optional type usually wraps another type provided as a generic type argument and provides a couple of methods: a hasValue() method, which tells us whether we have an actual value, and a getValue(), which returns that value. Attempting to call getValue() when no value is set causes an exception to be thrown, as shown in the next listing.
可选类 <T> { 1
私有值:T | 不明确的;
私人分配:布尔值;
构造函数(值?:T){ 2
如果(值){
this.value = 值;
this.assigned = true;
} 别的 {
this.value = undefined;
this.assigned = false;
}
}
有值():布尔值{
返回this.assigned;
}
getValue(): T {
如果(!this.assigned)抛出错误(); 3个
返回<T>这个值;
}
}class Optional<T> { 1
private value: T | undefined;
private assigned: boolean;
constructor(value?: T) { 2
if (value) {
this.value = value;
this.assigned = true;
} else {
this.value = undefined;
this.assigned = false;
}
}
hasValue(): boolean {
return this.assigned;
}
getValue(): T {
if (!this.assigned) throw Error(); 3
return <T>this.value;
}
}
在其他没有 | 的语言中 允许我们定义T | undefined类型的类型运算符,我们将改用可空类型。可为null 的 类型允许or 类型的任何值null,这表示没有值。
In other languages that don’t have a | type operator that allows us to define a T | undefined type, we would use a nullable type instead. A nullable type allows for any value of the type or null, which represents the absence of a value.
你可能想知道为什么这个可选类型有用,考虑到在大多数语言中,引用类型是允许的null,所以已经有一种方法可以在不需要这种类型的情况下编码“无可用值”。
You might wonder why this optional type is useful, considering that in most languages, reference types are allowed to be null, so there is already a way to encode “no value available” without needing such a type.
不同之处在于 usingnull容易出错(请参阅边栏“十亿美元的错误”),因为很难判断变量何时可以或不可以null。我们必须null在整个代码中添加检查,否则有取消引用null变量的风险,这会导致运行时错误。可选类型背后的想法是将null与允许值的范围分离。每当我们看到一个可选的,我们就知道它没有价值。在我们检查我们确实有一个值之后,我们将它从可选中解包并得到一个底层类型的变量。从这里开始,我们知道变量不能是null。这种区别体现在类型系统中,因为“might be null”变量具有不同的类型(DayOfWeek | undefined或Optional<DayOfWeek>) 来自展开的值,我们知道它不能是null( DayOfWeek)。可选类型与其底层类型不兼容会有所帮助,因此我们不会在未显式解包值的情况下意外使用可选类型(可能没有值)而不是其底层类型。
The difference is that using null is error-prone (see the sidebar “A billion-dollar mistake”), as it’s hard to tell when a variable can or can’t be null. We must add null checks all over the code or risk dereferencing a null variable, which results in a run-time error. The idea behind an optional type is to decouple the null from the range of allowed values. Whenever we see an optional, we know that it can have no value. After we check that we indeed have a value, we unwrap it from the optional and get a variable of the underlying type. From here on, we know that the variable cannot be null. This distinction is captured in the type system, as the “might be null” variable has a different type (DayOfWeek | undefined or Optional<DayOfWeek>) from the unwrapped value, which we know can’t be null (DayOfWeek). It helps that an optional type and its underlying type are incompatible, so we can’t accidentally use an optional (which may not have a value) instead of its underlying type without explicitly unwrapping the value.
著名计算机科学家和图灵奖获得者托尼·霍尔爵士称其null为“数十亿美元的错误”。引用他的话说:
Famous computer scientist and Turing Award winner Sir Tony Hoare calls null references his “billion-dollar mistake.” He is quoted as saying:
“我称之为我的十亿美元错误。它是 1965 年空引用的发明。当时,我正在设计面向对象语言中的第一个引用综合类型系统。我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但我无法抗拒放入空引用的诱惑,只是因为它很容易实现。这导致了无数的错误、漏洞和系统崩溃,在过去四十年中可能造成了十亿美元的痛苦和损失。”
“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”
在几十年的null取消引用错误之后,越来越清楚的是null,如果 或值的缺失本身不是类型的有效值,则更好。
After decades of null dereference errors, it’s becoming clear that it is better if null, or the absence of the value, is not itself a valid value of a type.
让我们扩展我们的DayOfWeek字符串转换示例,以便在我们无法确定值时不返回任何值DayOfWeek,而是返回更详细的错误信息。我们想要区分字符串何时为空以及何时无法解析它。如果我们在文本输入控件后面运行此代码,这将很有用,因为我们希望根据错误(Please enter a day of week与Invalid day of week)向用户显示不同的错误消息。
Let’s extend our DayOfWeek string conversion example so that instead of simply returning no value when we cannot determine the DayOfWeek value, we return more detailed error information. We want to distinguish between when the string is empty and when we are unable to parse it. This is useful if we run this code behind a text input control, as we want to show different error messages to the user, depending on the error (Please enter a day of week versus Invalid day of week).
一个常见的反模式会同时返回一个错误DayOfWeek代码和一个错误代码,如下一个清单所示。如果错误代码指示成功,我们将使用该DayOfWeek值。如果错误代码表示错误,该DayOfWeek值无效,我们不应该使用它。
A common antipattern returns both a DayOfWeek and an error code, as shown in the next listing. If the error code indicates success, we use the DayOfWeek value. If the error code indicates an error, the DayOfWeek value is invalid, and we shouldn’t use it.
枚举输入错误 { 1
好的,
没有输入,
无效的
}
类结果{#B
错误:输入错误;
值:星期几;
构造函数(错误:InputError,值:DayOfWeek){
this.error = 错误;
this.value = 值;
}
}
函数 parseDayOfWeek(输入:字符串):结果{ 2
如果(输入==“”)
返回新结果(InputError.NoInput,DayOfWeek.Sunday); 3个
开关(输入。toLowerCase()){
案例“星期天”:
返回新结果(InputError.OK,DayOfWeek.Sunday); 4个
案例“星期一”:
返回新结果(InputError.OK,DayOfWeek.Monday);
案例“星期二”:
返回新结果(InputError.OK,DayOfWeek.Tuesday);
案例“星期三”:
返回新结果(InputError.OK,DayOfWeek.Wednesday);
案例“星期四”:
返回新结果(InputError.OK,DayOfWeek.Thursday);
案例“星期五”:
返回新结果(InputError.OK,DayOfWeek.Friday);
案例“星期六”:
返回新结果(InputError.OK,DayOfWeek.Saturday);
默认:
返回新结果(InputError.Invalid,DayOfWeek.Sunday); 5个
}
}enum InputError { 1
OK,
NoInput,
Invalid
}
class Result { #B
error: InputError;
value: DayOfWeek;
constructor(error: InputError, value: DayOfWeek) {
this.error = error;
this.value = value;
}
}
function parseDayOfWeek(input: string): Result { 2
if (input == "")
return new Result(InputError.NoInput, DayOfWeek.Sunday); 3
switch (input.toLowerCase()) {
case "sunday":
return new Result(InputError.OK, DayOfWeek.Sunday); 4
case "monday":
return new Result(InputError.OK, DayOfWeek.Monday);
case "tuesday":
return new Result(InputError.OK, DayOfWeek.Tuesday);
case "wednesday":
return new Result(InputError.OK, DayOfWeek.Wednesday);
case "thursday":
return new Result(InputError.OK, DayOfWeek.Thursday);
case "friday":
return new Result(InputError.OK, DayOfWeek.Friday);
case "saturday":
return new Result(InputError.OK, DayOfWeek.Saturday);
default:
return new Result(InputError.Invalid, DayOfWeek.Sunday); 5
}
}
这并不理想,因为如果我们不小心忘记检查错误代码,没有什么能阻止我们使用该DayOfWeek成员。现在该值可以是默认值,并且我们不一定能说它是无效的。我们可能会通过系统传播错误,例如将其写入数据库,而没有意识到我们根本不应该使用该值。
This is not ideal because if we accidentally forget to check the error code, nothing prevents us from using the DayOfWeek member. Now the value can be a default, and we aren’t necessarily able to tell that it is invalid. We might propagate the error through the system, such as writing it to a database, without realizing that we shouldn’t have used the value at all.
从类型作为集合的角度来看,我们的结果包含所有可能的错误代码和所有可能的结果的组合(图 3.4)。
Looking at this from the lens of types as sets, our result contains the combination of all possible error codes and all possible results (figure 3.4).
相反,我们应该尝试返回错误或有效值。如果我们设法做到这一点,可能值的集合将大大减少,并且我们消除了使用组件是或的组件DayOfWeek的可能性(图 3.5)。 ResultInputErrorNoInputInvalid
Instead, we should try to return either an error or a valid value. If we manage to do that, the set of possible values is drastically decreased, and we eliminate the possibility of using the DayOfWeek component of a Result in which the InputError component is NoInput or Invalid (figure 3.5).
一个Either类型包含两种类型,TLeft和TRight,约定是TLeft存储错误类型和TRight存储有效值类型。(如果没有错误,则值为“正确”。)同样,一些编程语言将此作为其库的一部分提供,但如果需要,我们可以轻松实现这样的类型。
An Either type wraps two types, TLeft and TRight, the convention being that TLeft stores the error type and TRight stores the valid value type. (If there’s no error, the value is “right”.) Again, some programming languages provide this as part of their library, but if necessary, we can easily implement such a type.
类 Either<TLeft, TRight> {
私有只读值:TLeft | 对; 1
个私有只读左:布尔值; 1个
私有构造函数(值:TLeft | TRight,左:布尔值){ 2
this.value = 值;
this.left = 左;
}
isLeft(): 布尔值 {
返回 this.left;
}
getLeft(): TLeft { 3
如果(!this.isLeft())抛出新的错误();
返回 <TLeft>this.value;
}
isRight(): 布尔值 {
返回 !this.left;
}
getRight(): TRight { 3
如果(!this.isRight())抛出新的错误();
返回 <TRight>this.value;
}
static makeLeft<TLeft, TRight>(value: TLeft) { 4
返回新的 Either<TLeft, TRight>(value, true);
}
static makeRight<TLeft, TRight>(value: TRight) { 4
返回新的 Either<TLeft, TRight>(value, false);
}
}class Either<TLeft, TRight> {
private readonly value: TLeft | TRight; 1
private readonly left: boolean; 1
private constructor(value: TLeft | TRight, left: boolean) { 2
this.value = value;
this.left = left;
}
isLeft(): boolean {
return this.left;
}
getLeft(): TLeft { 3
if (!this.isLeft()) throw new Error();
return <TLeft>this.value;
}
isRight(): boolean {
return !this.left;
}
getRight(): TRight { 3
if (!this.isRight()) throw new Error();
return <TRight>this.value;
}
static makeLeft<TLeft, TRight>(value: TLeft) { 4
return new Either<TLeft, TRight>(value, true);
}
static makeRight<TLeft, TRight>(value: TRight) { 4
return new Either<TLeft, TRight>(value, false);
}
}
在缺少类型运算符 | 的语言中,我们可以简单地将值设为通用类型,例如Object在 Java 和 C# 中。andgetLeft()方法getRight()处理返回到TLeftandTRight类型的转换。
In a language that’s missing the type operator |, we could simply make the value a common type, such as Object in Java and C#. The getLeft() and getRight() methods handle conversion back to the TLeft and TRight types.
使用这样的类型,我们可以更新我们的parseDayOfWeek()实现以返回Either<InputError, DayOfWeek>结果并使其无法传播无效或默认DayOfWeek值。如果该函数返回一个,则结果中InputError没有,并且尝试通过调用 解包一个抛出错误。 DayOfWeekgetLeft()
With such a type, we can update our parseDayOfWeek() implementation to return an Either<InputError, DayOfWeek> result and make it impossible to propagate an invalid or default DayOfWeek value. If the function returns an InputError, there is no DayOfWeek in the result, and attempting to unwrap one via a call to getLeft() throws an error.
同样,我们必须明确解包值。当我们知道我们有一个有效值(isLeft()returns true),并且我们用 提取它时getLeft(),我们保证有有效数据。
Again, we have to be explicit about unpacking the value. When we know that we have a valid value (isLeft() returns true), and we extract it with getLeft(), we are guaranteed to have valid data.
枚举输入错误 { 1
没有输入,
无效的
}
类型 Result = Either<InputError, DayOfWeek>; 2个
函数 parseDayOfWeek(输入:字符串):结果 {
如果(输入==“”)
返回 Either.makeLeft(InputError.NoInput); 3个
开关(输入。toLowerCase()){ 3
案例“星期天”:
返回 Either.makeRight(DayOfWeek.Sunday);
案例“星期一”:
返回 Either.makeRight(DayOfWeek.Monday);
案例“星期二”:
返回 Either.makeRight(DayOfWeek.Tuesday);
案例“星期三”:
返回 Either.makeRight(DayOfWeek.Wednesday);
案例“星期四”:
返回 Either.makeRight(DayOfWeek.Thursday);
案例“星期五”:
返回 Either.makeRight(DayOfWeek.Friday);
案例“星期六”:
返回 Either.makeRight(DayOfWeek.Saturday);
默认:
返回 Either.makeLeft(InputError.Invalid); 3个
}
}enum InputError { 1
NoInput,
Invalid
}
type Result = Either<InputError, DayOfWeek>; 2
function parseDayOfWeek(input: string): Result {
if (input == "")
return Either.makeLeft(InputError.NoInput); 3
switch (input.toLowerCase()) { 3
case "sunday":
return Either.makeRight(DayOfWeek.Sunday);
case "monday":
return Either.makeRight(DayOfWeek.Monday);
case "tuesday":
return Either.makeRight(DayOfWeek.Tuesday);
case "wednesday":
return Either.makeRight(DayOfWeek.Wednesday);
case "thursday":
return Either.makeRight(DayOfWeek.Thursday);
case "friday":
return Either.makeRight(DayOfWeek.Friday);
case "saturday":
return Either.makeRight(DayOfWeek.Saturday);
default:
return Either.makeLeft(InputError.Invalid); 3
}
}
更新后的实现利用类型系统来消除无效状态,例如(NoInput, Sunday)我们可能不小心使用了该Sunday值。此外,也不需要为OK值,InputError因为如果解析成功,我们就不会出现错误。
The updated implementation leverages the type system to eliminate invalid states such as (NoInput, Sunday) from which we could’ve accidentally used the Sunday value. Also, there’s no need for an OK value for InputError because we don’t have an error if parsing succeeds.
错误抛出异常是结果或错误的一个完全有效的例子:函数要么返回结果,要么抛出异常。在某些情况下,不能使用异常,而是Either首选类型,例如在跨进程或跨线程传播错误时;作为设计原则,当错误本身并不例外时(通常是我们处理用户输入的情况);调用使用错误代码的操作系统 API 时;等等。在这些情况下,当我们不能或不想抛出异常但需要传达我们得到一个值或失败时,最好将其编码为值或错误,而不是值和错误。
Throwing an exception on error is a perfectly valid example of result or error: the function either returns a result or throws an exception. In several situations, exceptions cannot be used and an Either type is preferred, such as when propagating errors across processes or across threads; as a design principle, when the error itself is not exceptional (often the case when we deal with user input); when calling operating system APIs that use error codes; and so on. In these situations, when we can’t or don’t want to throw an exception but need to communicate that we got a value or failed, it’s best to encode this as an either value or error as opposed to value and error.
当抛出异常是可接受的时,我们可以将它们用作另一种方式来确保我们不会以无效结果 和错误结束。当抛出异常时,该函数不再通过使用语句将值传回给调用者来返回“正常”方式return。catch相反,它会传播异常对象,直到找到匹配为止。这样,我们得到一个结果或异常。我们不会深入讨论抛出异常,因为尽管许多语言都提供了抛出和捕获异常的功能,但从类型的角度来看,异常并不是很特别。
When throwing exceptions is acceptable, we can use them as another way to ensure that we don’t end up with an invalid result and an error. When an exception is thrown, the function no longer returns the “normal” way, by passing back a value to the caller with a return statement. Rather, it propagates the exception object until a matching catch is found. This way, we get a result or an exception. We won’t cover throwing exceptions in depth, because although many languages provide facilities for exceptions to be thrown and caught, from a type perspective, exceptions aren’t very special.
我们已经了解了可选类型,它包含基础类型的值或没有值。然后我们查看了either包含 aTLeft或TRight值的类型。这些类型的概括是变体类型。
We’ve looked at optional types, which contain a value of the underlying type or no value. Then we looked at either types, which contain a TLeft or a TRight value. The generalizations of these types are the variant types.
变体类型,也称为标记联合类型,包含任意数量的基础类型的值。标记来自这样一个事实,即即使底层类型具有重叠的值,我们仍然能够准确地分辨出该值来自哪个类型。
Variant types, also known as tagged union types, contain a value of any number of underlying types. Tagged comes from the fact that even if the underlying types have overlapping values, we are still able to tell exactly which type the value comes from.
让我们看一下清单 3.16中几何形状集合的示例。每个形状都有一组不同的属性和一个标签(作为kind属性实现)。我们可以定义一个类型,它是所有这些形状的联合。然后,当我们想要(例如)渲染这些形状时,我们可以使用它们的kind属性来确定实例是哪些可能的形状,然后将其转换为该形状。此过程与前面示例中的展开相同。
Let’s look at an example of a collection of geometric shapes in listing 3.16. Each shape has a different set of properties and a tag (implemented as a kind property). We can define a type that is the union of all these shapes. Then, when we want to (for example) render these shapes, we can use their kind property to determine which of the possible shapes an instance is, then cast it to that shape. This process is the same as the unwrapping in previous examples.
类点{
只读种类:字符串=“点”;
x: 数字 = 0;
y: 数字 = 0;
}
类圈子{
只读类型:string = "Circle";
x: 数字 = 0;
y: 数字 = 0;
半径:数字= 0;
}
类矩形{
只读种类:字符串=“矩形”;
x: 数字 = 0;
y: 数字 = 0;
宽度:数字 = 0;
高度:数字= 0;
}
输入形状 = 点 | 圈子 | 长方形;
让形状:Shape[] = [new Circle(), new Rectangle()];
for (let shape of shapes) { 1
switch (shape.kind) { 1
案例“点”:
让点:Point = <Point>shape; 2个
console.log(`Point ${JSON.stringify(point)}`);
休息;
案例“圆”:
let circle: Circle = <Circle>shape; 2个
console.log(`Circle ${JSON.stringify(circle)}`);
休息;
案例“矩形”:
let rectangle: Rectangle = <Rectangle>shape; 2个
console.log(`矩形 ${JSON.stringify(rectangle)}`);
休息;
默认值: 3
抛出新的错误();
}
}class Point {
readonly kind: string = "Point";
x: number = 0;
y: number = 0;
}
class Circle {
readonly kind: string = "Circle";
x: number = 0;
y: number = 0;
radius: number = 0;
}
class Rectangle {
readonly kind: string = "Rectangle";
x: number = 0;
y: number = 0;
width: number = 0;
height: number = 0;
}
type Shape = Point | Circle | Rectangle;
let shapes: Shape[] = [new Circle(), new Rectangle()];
for (let shape of shapes) { 1
switch (shape.kind) { 1
case "Point":
let point: Point = <Point>shape; 2
console.log(`Point ${JSON.stringify(point)}`);
break;
case "Circle":
let circle: Circle = <Circle>shape; 2
console.log(`Circle ${JSON.stringify(circle)}`);
break;
case "Rectangle":
let rectangle: Rectangle = <Rectangle>shape; 2
console.log(`Rectangle ${JSON.stringify(rectangle)}`);
break;
default: 3
throw new Error();
}
}
在前面的示例中,kind每个类的成员代表告诉我们值的实际类型的标记。的值shape.kind告诉我们 Shape 实例是Point、Circle还是Rectangle。我们还可以实现一个通用的变体来跟踪类型,而不需要类型本身存储标签。
In the preceding example, the kind member of each class represents the tag which tells us the actual type of a value. The value of shape.kind tells us whether the Shape instance is a Point, Circle, or Rectangle. We can also implement a general--purpose variant that keeps track of the types without requiring the types themselves to store a tag.
让我们实现一个简单的变体,它最多可以存储三种类型的值,并根据类型索引跟踪存储的实际类型。
Let’s implement a simple variant that can store a value of up to three types and keep track of the actual type stored based on a type index.
不同的编程语言提供不同的通用和类型检查功能。例如,某些语言允许可变数量的泛型参数(因此我们可以拥有任意数量类型的变体);其他人提供了不同的方法来在编译和运行时确定一个值是否属于某种类型。
Different programming languages provide different generic and type-checking features. Some languages allow a variable number of generic arguments, for example (so we can have variants of any number of types); others provide different ways to determine whether a value is of a certain type at both compile and run time.
以下 TypeScript 实现有一些不一定可以转换为其他编程语言的权衡。它是通用变体的起点,但它会以不同的方式实现,例如,Java 或 C#。例如,TypeScript 不支持方法重载,但在其他语言中,我们可以在每个泛型类型上重载一个make()函数。
The following TypeScript implementation has some trade-offs that don’t necessarily translate to other programming languages. It’s a starting point for a general--purpose variant, but it would be implemented differently in, say, Java or C#. TypeScript doesn’t support method overloads, for example, but in other languages, we could get away with a single make() function overloaded on each generic type.
类变体<T1, T2, T3> {
只读值:T1 | T2 | T3;
只读索引:数字;
私有构造函数(值:T1 | T2 | T3,索引:数字){
this.value = 值;
this.index = 索引;
}
static make1<T1, T2, T3>(值:T1):变体<T1, T2, T3> {
返回新变量<T1, T2, T3>(值, 0);
}
static make2<T1, T2, T3>(值:T2):变体<T1,T2,T3> {
返回新变量<T1, T2, T3>(值, 1);
}
static make3<T1, T2, T3>(值: T3): 变体<T1, T2, T3> {
返回新变量<T1, T2, T3>(值, 2);
}
}class Variant<T1, T2, T3> {
readonly value: T1 | T2 | T3;
readonly index: number;
private constructor(value: T1 | T2 | T3, index: number) {
this.value = value;
this.index = index;
}
static make1<T1, T2, T3>(value: T1): Variant<T1, T2, T3> {
return new Variant<T1, T2, T3>(value, 0);
}
static make2<T1, T2, T3>(value: T2): Variant<T1, T2, T3> {
return new Variant<T1, T2, T3>(value, 1);
}
static make3<T1, T2, T3>(value: T3): Variant<T1, T2, T3> {
return new Variant<T1, T2, T3>(value, 2);
}
}
此实现负责维护标签,因此现在我们可以将它们从我们的几何形状中删除。
This implementation takes on the responsibility of maintaining the tags, so now we can remove them from our geometric shapes.
类点{ 1
x: 数字 = 0;
y: 数字 = 0;
}
类圆{ 1
x: 数字 = 0;
y: 数字 = 0;
半径:数字= 0;
}
矩形类 { 1
x: 数字 = 0;
y: 数字 = 0;
宽度:数字 = 0;
高度:数字= 0;
}
type Shape = Variant<Point, Circle, Rectangle> ; 2个
让形状:Shape[] = [
Variant.make2(new Circle()),
Variant.make3(新矩形())
];
对于(让形状的形状){
开关(shape.index){ 3
案例 0:
让点:Point = <Point>shape.value; 3个
console.log(`Point ${JSON.stringify(point)}`);
休息;
情况1:
let circle: Circle = <Circle>shape.value;
console.log(`Circle ${JSON.stringify(circle)}`);
休息;
案例 2:
let rectangle: Rectangle = <Rectangle>shape.value;
console.log(`矩形 ${JSON.stringify(rectangle)}`);
休息;
默认:
抛出新的错误();
}
}class Point { 1
x: number = 0;
y: number = 0;
}
class Circle { 1
x: number = 0;
y: number = 0;
radius: number = 0;
}
class Rectangle { 1
x: number = 0;
y: number = 0;
width: number = 0;
height: number = 0;
}
type Shape = Variant<Point, Circle, Rectangle>; 2
let shapes: Shape[] = [
Variant.make2(new Circle()),
Variant.make3(new Rectangle())
];
for (let shape of shapes) {
switch (shape.index) { 3
case 0:
let point: Point = <Point>shape.value; 3
console.log(`Point ${JSON.stringify(point)}`);
break;
case 1:
let circle: Circle = <Circle>shape.value;
console.log(`Circle ${JSON.stringify(circle)}`);
break;
case 2:
let rectangle: Rectangle = <Rectangle>shape.value;
console.log(`Rectangle ${JSON.stringify(rectangle)}`);
break;
default:
throw new Error();
}
}
这个实现可能看起来并没有增加很多好处;我们最终使用数字标签并任意决定 0 是 aPoint和 1 是 a Circle。您可能还想知道为什么我们不为我们的形状使用类层次结构,我们有一个基本方法,每个类型都实现而不是切换标签。
This implementation might not look as though it adds a lot of benefit; we ended up using numeric tags and arbitrarily decided that 0 is a Point and 1 is a Circle. You might also wonder why we didn’t use a class hierarchy for our shapes, where we have a base method that each type implements instead of switching on tags.
对于该任务,我们需要了解访问者设计模式及其实现方式。
For that task, we need to take a look at the visitor design pattern and the ways in which it can be implemented.
用户可以在红色、绿色和蓝色中进行选择。这个选择的类型应该是什么?
Users can provide a selection among the colors red, green, and blue. What should be the type of this selection?
将字符串作为输入并将其解析为数字的函数的返回类型应该是什么?该函数不会抛出。
- number
- number | undefined
- Optional<number>
- b或c
What should be the return type of a function that takes a string as input and parses it into a number? The function does not throw.
- number
- number | undefined
- Optional<number>
- Either b or c
操作系统通常使用数字来表示错误代码。可以返回数值或数字错误代码的函数的返回类型应该是什么?
- number
- { value: number, error: number }
- number | number
- Either<number, number>
Operating systems usually use numbers to represent error codes. What should be the return type of a function that can return either a numerical value or a numerical error code?
- number
- { value: number, error: number }
- number | number
- Either<number, number>
让我们回顾一下访问者设计模式,看看遍历构成文档的项目——首先通过面向对象的镜头,然后使用我们实现的通用标记联合类型。如果您对访问者设计模式不是很熟悉,请不要担心;我们将在完成示例时回顾它的工作原理。
Let’s go over the visitor design pattern and look at traversing the items that make up a document—first through an object-oriented lens and then with the generic tagged union type we implemented. Don’t worry if you aren’t very familiar with the visitor design pattern; we’ll review how it works as we’re working through our example.
我们将从一个朴素的实现开始,展示访问者设计模式如何改进设计,然后展示一个替代的实现,它消除了类层次结构的需要。
We’ll start with a naïve implementation, show how the visitor design pattern improves the design, and then show an alternative implementation that removes the need for class hierarchies.
我们从三个文档项目开始:段落、图片和表格。我们希望将它们呈现在屏幕上,或者让屏幕阅读器为视障用户大声朗读它们。
We start with three document items: paragraph, picture, and table. We want to either render them onscreen or have a screen reader read them aloud for visually impaired users.
我们可以采取的一种方法是提供一个通用接口,以确保每个项目都知道如何在屏幕上绘制自己并读取自己,如下一个清单所示。
One approach we can take is to provide a common interface to ensure that each item knows how to draw itself on a screen and read itself, as shown in the next listing.
class Renderer { /* 渲染方法 */ } 1
class ScreenReader { /* 屏幕阅读方法 */ } 1
接口 IDocumentItem { 2
渲染(渲染器:渲染器):无效;
阅读(屏幕阅读器:屏幕阅读器):无效;
}
类段落实现 IDocumentItem { 3
/* 段落成员省略 */
渲染(渲染器:渲染器){
/* 使用渲染器在屏幕上绘制自己 */
}
阅读(屏幕阅读器:屏幕阅读器){
/* 使用 screenReader 读取自身 */
}
}
类图片实现 IDocumentItem { 3
/* 省略图片成员 */
渲染(渲染器:渲染器){
/* 使用渲染器在屏幕上绘制自己 */
}
阅读(屏幕阅读器:屏幕阅读器){
/* 使用 screenReader 读取自身 */
}
}
类表实现 IDocumentItem { 3
/* 省略表成员 */
渲染(渲染器:渲染器){
/* 使用渲染器在屏幕上绘制自己 */
}
阅读(屏幕阅读器:屏幕阅读器){
/* 使用 screenReader 读取自身 */
}
}
let doc: IDocumentItem[] = [new Paragraph(), new Table()];
让渲染器:渲染器=新渲染器();
for (let item of doc) {
item.render(渲染器);
}class Renderer { /* Rendering methods */ } 1
class ScreenReader { /* Screen reading methods */ } 1
interface IDocumentItem { 2
render(renderer: Renderer): void;
read(screenReader: ScreenReader): void;
}
class Paragraph implements IDocumentItem { 3
/* Paragraph members omitted */
render(renderer: Renderer) {
/* Uses renderer to draw itself on screen */
}
read(screenReader: ScreenReader) {
/* Uses screenReader to read itself */
}
}
class Picture implements IDocumentItem { 3
/* Picture members omitted */
render(renderer: Renderer) {
/* Uses renderer to draw itself on screen */
}
read(screenReader: ScreenReader) {
/* Uses screenReader to read itself */
}
}
class Table implements IDocumentItem { 3
/* Table members omitted */
render(renderer: Renderer) {
/* Uses renderer to draw itself on screen */
}
read(screenReader: ScreenReader) {
/* Uses screenReader to read itself */
}
}
let doc: IDocumentItem[] = [new Paragraph(), new Table()];
let renderer: Renderer = new Renderer();
for (let item of doc) {
item.render(renderer);
}
从设计的角度来看,这种方法并不好。文档项存储描述文档内容的信息,例如文本或图像,不应该负责其他事情,例如呈现和可访问性。在每个文档项类中包含呈现和可访问性代码会使代码膨胀。更糟糕的是,如果我们需要添加新功能(例如,用于打印),我们需要更新接口和所有实现类来实现新功能。
This approach is not great from a design point of view. The document items store information that describes document content, such as text or an image, and should not be responsible for other things, such as rendering and accessibility. Having rendering and accessibility code in each document item class bloats the code. Worse, if we need to add a new capability—say, for printing—we need to update the interface and all implementing classes to implement the new capability.
访问者模式是对对象结构的元素执行的操作。此模式允许您定义一个新操作,而无需更改它所操作的元素的类。
The visitor pattern is an operation to be performed on elements of an object structure. This pattern lets you define a new operation without changing the classes of the elements on which it operates.
在清单 3.20所示的示例中,该模式应该允许我们添加新功能而无需触及文档项的代码。我们可以使用双分派机制来完成这项任务,其中文档项接受任何访问者,然后将自己传递给它。访问者知道如何处理每个单独的项目(通过呈现、大声朗读等等),因此给定项目的实例,它会执行正确的操作(图 3.6 )。
In our example shown in listing 3.20, the pattern should allow us to add a new capability without having to touch the code of the document items. We can achieve this task with the double-dispatch mechanism, in which document items accept any visitor and then pass themselves to it. The visitor knows how to process each individual item (by rendering it, reading it aloud, and so on), so given an instance of the item, it performs the right operation (figure 3.6).
双重分派来自这样一个事实,即给定一个,首先调用IDocumentItem正确的方法;accept()然后,给定IVisitor参数,执行正确的操作。
Double dispatch comes from the fact that, given an IDocumentItem, the right accept() method is called first; then, given the IVisitor argument, the right operation is performed.
接口 IVisitor { 1
访问段落(段落:段落):无效;
访问图片(图片:图片):void;
访问表(表:表):无效;
}
类渲染器实现 IVisitor { 2
访问段落(段落:段落){ /* ... */ }
visitPicture(picture: 图片) { /* ... */ }
visitTable(table: Table) { /* ... */ }
}
类 ScreenReader 实现 IVisitor { 1
访问段落(段落:段落){ /* ... */ }
visitPicture(picture: 图片) { /* ... */ }
visitTable(table: Table) { /* ... */ }
}
接口 IDocumentItem { 3
接受(访客:IVisitor):无效;
}
类段落实现 IDocumentItem {
/* 段落成员省略 */
接受(访客:IVisitor){
visitor.visitParagraph(这个); 4个
}
}
类图片实现 IDocumentItem {
/* 省略图片成员 */
接受(访客:IVisitor){
visitor.visitPicture(这个); 4个
}
}
类表实现 IDocumentItem {
/* 省略表成员 */
接受(访客:IVisitor){
visitor.visitTable(这个); 4个
}
}
let doc: IDocumentItem[] = [new Paragraph(), new Table()];
让渲染器:IVisitor = new Renderer();
for (let item of doc) {
item.accept(渲染器);
}interface IVisitor { 1
visitParagraph(paragraph: Paragraph): void;
visitPicture(picture: Picture): void;
visitTable(table: Table): void;
}
class Renderer implements IVisitor { 2
visitParagraph(paragraph: Paragraph) { /* ... */ }
visitPicture(picture: Picture) { /* ... */ }
visitTable(table: Table) { /* ... */ }
}
class ScreenReader implements IVisitor { 1
visitParagraph(paragraph: Paragraph) { /* ... */ }
visitPicture(picture: Picture) { /* ... */ }
visitTable(table: Table) { /* ... */ }
}
interface IDocumentItem { 3
accept(visitor: IVisitor): void;
}
class Paragraph implements IDocumentItem {
/* Paragraph members omitted */
accept(visitor: IVisitor) {
visitor.visitParagraph(this); 4
}
}
class Picture implements IDocumentItem {
/* Picture members omitted */
accept(visitor: IVisitor) {
visitor.visitPicture(this); 4
}
}
class Table implements IDocumentItem {
/* Table members omitted */
accept(visitor: IVisitor) {
visitor.visitTable(this); 4
}
}
let doc: IDocumentItem[] = [new Paragraph(), new Table()];
let renderer: IVisitor = new Renderer();
for (let item of doc) {
item.accept(renderer);
}
现在访问者可以遍历一组IDocumentItem对象并通过调用accept()每个对象来处理它们。处理的责任从物品本身转移到访客身上。添加新访客不会影响文档项;新的访问者只需要实现IVisitor接口,文档项就会像接受其他任何东西一样接受它。
Now a visitor can go over a collection of IDocumentItem objects and process them by calling accept() on each. The responsibility of processing is moved from the items themselves to the visitors. Adding a new visitor does not affect the document items; the new visitor just needs to implement the IVisitor interface, and document items would accept it as they would any other.
新的访问者类将在、和方法Printer中实现打印段落、图片和表格的逻辑。文档项目本身无需更改即可打印。 visitParagraph()visitPicture()visitTable()
A new Printer visitor class would implement logic to print a paragraph, a picture, and a table in the visitParagraph(), visitPicture(), and visitTable() methods. The document items themselves would become printable without having to change.
这个例子是访问者模式的经典实现。接下来,让我们看看如何通过使用变体来实现类似的效果。
This example is a classical implementation of the visitor pattern. Next, let’s look at how we could achieve something similar by using a variant instead.
首先,让我们回到我们的通用变体类型并实现一个visit()函数,该函数接受一个变体和一组函数,每个类型一个,并且(取决于存储在变体中的值)将正确的函数应用于它。
First, let’s go back to our generic variant type and implement a visit() function that takes a variant and a set of functions, one for each type, and (depending on the value stored in the variant) applies the right function to it.
函数访问<T1, T2, T3>(
变体:变体<T1,T2,T3>,
func1: (value: T1) => void, 1
func2: (value: T2) => void, 1
func3: (value: T3) => void 1
): 空白 {
开关(变体索引){
案例 0: func1(<T1>variant.value); 休息; 2
案例 1:func2(<T2>variant.value); 休息; 2
案例 2:func3(<T3>variant.value); 休息; 2个
默认值:抛出新的错误();
}
}function visit<T1, T2, T3>(
variant: Variant<T1, T2, T3>,
func1: (value: T1) => void, 1
func2: (value: T2) => void, 1
func3: (value: T3) => void 1
): void {
switch (variant.index) {
case 0: func1(<T1>variant.value); break; 2
case 1: func2(<T2>variant.value); break; 2
case 2: func3(<T3>variant.value); break; 2
default: throw new Error();
}
}
如果我们将文档项放在变体中,我们可以使用此函数来选择适当的访问者方法。如果我们这样做,我们就不再需要强制我们的任何类实现某些接口:将正确的文档项与正确的处理方法相匹配的责任转移到这个通用函数中visit()。
If we place our document items in a variant, we can use this function to select the appropriate visitor method. If we do this, we no longer have to force any of our classes to implement certain interfaces: responsibility for matching the right document item with the right processing method is moved to this generic visit() function.
文档项不再需要了解有关访问者的任何信息,也不需要“接受”他们,如以下清单所示。
Document items no longer need to know anything about visitors and don’t need to “accept” them, as the following listing shows.
类渲染器{
渲染段落(段落:段落){ /* ... */ }
渲染图片(图片:图片){ /* ... */ }
renderTable(table: Table) { /* ... */ }
}
类屏幕阅读器{
readParagraph(段落: 段落) { /* ... */ }
readPicture(图片:图片) { /* ... */ }
readTable(table: Table) { /* ... */ }
}
类段落 { 1
/* 段落成员省略 */
}
类图片 { 1
/* 省略图片成员 */
}
类表 { 1
/* 省略表成员 */
}
let doc: Variant<段落, 图片, 表格>[] = [ 2
Variant.make1(新段落()),
Variant.make3(新表())
];
让渲染器:渲染器=新渲染器();
for (let item of doc) {
访问(项目, 3
(段落:段落)=> renderer.renderParagraph(段落),
(图片: 图片) => renderer.renderPicture(图片),
(表:表)=> renderer.renderTable(表)
);
}class Renderer {
renderParagraph(paragraph: Paragraph) { /* ... */ }
renderPicture(picture: Picture) { /* ... */ }
renderTable(table: Table) { /* ... */ }
}
class ScreenReader {
readParagraph(paragraph: Paragraph) { /* ... */ }
readPicture(picture: Picture) { /* ... */ }
readTable(table: Table) { /* ... */ }
}
class Paragraph { 1
/* Paragraph members omitted */
}
class Picture { 1
/* Picture members omitted */
}
class Table { 1
/* Table members omitted */
}
let doc: Variant<Paragraph, Picture, Table>[] = [ 2
Variant.make1(new Paragraph()),
Variant.make3(new Table())
];
let renderer: Renderer = new Renderer();
for (let item of doc) {
visit(item, 3
(paragraph: Paragraph) => renderer.renderParagraph(paragraph),
(picture: Picture) => renderer.renderPicture(picture),
(table: Table) => renderer.renderTable(table)
);
}
通过这种方法,我们将双重分派机制与我们正在使用的类型分离,并将其移至变体/访问者。变体和访问者是可以跨不同问题域重用的通用类型。这种方法的优点是它让访问者只负责处理,文档项只负责存储域数据(图 3.7)。
With this approach, we decouple the double-dispatch mechanism from the types we are using and move it to the variant/visitor. The variant and visitor are generic types that can be reused across different problem domains. The advantage of this approach is that it lets visitors be responsible only for processing and document items be responsible only for storing domain data (figure 3.7).
我们介绍的函数visit()也是使用变体类型的预期方式。当我们想要准确地找出变体包含的类型时,对变体的索引执行切换可能很容易出错。但是通常,一旦我们有了一个变体,我们就不想提取它的价值;相反,我们通过使用visit(). 这样容易出错的switch在visit()实现中就处理好了,我们就不用操心了。将容易出错的代码封装在可重用的组件中是降低风险的好做法,因为当实现稳定并经过测试时,我们可以在多个场景中依赖它。
The visit() function we introduced is also the expected way to use a variant type. Performing a switch on the index of the variant when we want to figure out exactly which type it contains could be error-prone. But usually, once we have a variant, we don’t want to extract the value; instead, we apply functions to it by using visit(). This way, the error-prone switch is handled in the visit() implementation, and we don’t have to worry about it. Encapsulating error-prone code in a reusable component is good practice for reducing risk, because when the implementation is stable and tested, we can rely on it in multiple scenarios.
使用基于变体的访问者而不是经典的 OOP 实现的优点是它将我们的领域对象与访问者完全分开。现在我们甚至不需要accept()方法,文档项也不需要知道是什么在处理它们。它们也不必符合任何特定的接口,例如IDocumentItem在我们的示例中。这是因为将访问者与形状匹配的胶水代码封装在Variant及其visit()功能中。
Using a variant-based visitor instead of the classical OOP implementation has the advantage that it fully separates our domain objects from the visitors. Now we don’t even need an accept() method, and document items don’t need to know anything about what is processing them. They also don’t have to conform to any particular interface, such as IDocumentItem in our example. That’s because the glue code that matches visitors with shapes is encapsulated in Variant and its visit() function.
我们的visit()实施返回void。扩展它,以便给定 a ,它通过应用以下三个函数之一Variant<T1, T2, T3>返回 a : , or , or 。 Variant<U1, U2, U3>(value: T1) => U1(value: T2) => U2(value: T3) => U3
Our visit() implementation returns void. Extend it so that given a Variant<T1, T2, T3>, it returns a Variant<U1, U2, U3> by applying one of three functions: (value: T1) => U1, or (value: T2) => U2, or (value: T3) => U3.
您可能听说过代数数据类型(ADT)一词。ADT 是在类型系统中组合类型的方法。事实上,这正是我们在本章中所涵盖的内容。ADT 提供了两种组合类型的方法:乘积类型和求和类型。
You might have heard the term algebraic data types (ADTs). ADTs are ways to combine types within a type system. In fact, this is exactly what we covered during this chapter. ADTs provide two ways to combine types: product types and sum types.
产品类型就是我们在本章中所说的复合类型。元组和记录是产品类型,因为它们的值是它们的组合类型的产品。类型 A = {a1, a2}(type Awith possible values a1and a2) 和B = {b1, b2}(type Bwith possible values b1and b2) 组合成元组类型<A, B>作为A x B = {(a1, b1), (a1, b2), (a2, b1), (a2, b2)}.
Product types are what we called compound types in this chapter. Tuples and records are product types because their values are products of their composing types. The types A = {a1, a2} (type A with possible values a1 and a2) and B = {b1, b2} (type B with possible values b1 and b2) combine into the tuple type <A, B> as A x B = {(a1, b1), (a1, b2), (a2, b1), (a2, b2)}.
产品类型将多个其他类型组合成一个新类型,该新类型存储每个组合类型的值。A类型、B和的产品类型C——我们可以写成A x B x C——包含一个来自 的值A、一个来自 的值B和一个来自 的值。元组和记录类型是产品类型的示例。此外,记录允许我们为它们的每个组件分配有意义的名称。 C
Product types combine multiple other types into a new type that stores a value from each of the combined types. The product type of types A, B, and C—which we can write as A x B x C—contains a value from A, a value from B, and a value from C. Tuple and record types are examples of product types. Additionally, records allow us to assign meaningful names to each of their components.
记录类型应该非常熟悉,因为它们通常是新程序员学习的第一个组合方法。最近,元组已进入主流编程语言,但它们应该不会特别难理解。元组与记录类型非常相似,除了我们不能命名它们的成员并且通常可以通过指定构成元组的类型来内联定义它们。例如,在 TypeScript 中,[number, number]定义了由两个值组成的元组类型number。
Record types should be very familiar, as they are usually the first composition method that new programmers learn. Recently, tuples have made their way into mainstream programming languages, but they shouldn’t be particularly hard to understand. Tuples are very similar to record types except that we can’t name their members and usually can define them inline by specifying the types that make up the tuple. In TypeScript, for example, [number, number] defines the tuple type composed of two number values.
我们在求和类型之前介绍了产品类型,因为它们应该更熟悉。几乎所有的编程语言都提供了定义记录类型的方法。较少的主流语言为求和类型提供句法支持。
We covered product types before sum types, as they should be more familiar. Almost all programming languages provide ways to define record types. Fewer mainstream languages provide syntactic support for sum types.
Sum 类型就是我们在本章前面所说的either-or类型。它们通过允许来自任何一种类型的值来组合类型,但只能是其中一种。类型A = {a1, a2}和B = {b1, b2}合并为总和类型A | Bas A + B = {a1, a2, b1, b2}。
Sum types are what we called either-or types earlier in this chapter. They combine types by allowing a value from any one of the types, but only one of them. The types A = {a1, a2} and B = {b1, b2} combine into the sum type A | B as A + B = {a1, a2, b1, b2}.
Sum 类型将多个其他类型组合成一个新类型,该新类型存储来自任何一种组合类型的值。类型A、B和的总和类型C——我们可以写成——A + B + C包含一个来自 的值A,或一个来自 的值B,或一个来自 的值C。可选类型和变体类型是总和类型的示例。
Sum types combine multiple other types into a new type that stores a value from any one of the combined types. The sum type of types A, B, and C—which we can write as A + B + C—contains a value from A, or a value from B, or a value from C. Optional and variant types are examples of sum types.
正如我们所见,TypeScript 有 | 类型运算符,但可以在没有它的情况下实现常见的求和类型,例如Optional,Either和。Variant这些类型提供了强大的方法来表示结果或错误以及类型的封闭集,并启用了不同的方法来实现常见的访问者模式。
As we saw, TypeScript has the | type operator, but common sum types such as Optional , Either, and Variant can be implemented without it. These types provide powerful ways for representing result or error and closed sets of types, and enable different ways to implement the common visitor pattern.
通常,总和类型允许我们将不相关类型的值存储在单个变量中。与访问者模式示例一样,面向对象的替代方案是使用公共基类或接口,但扩展性不佳。如果我们在应用程序的不同地方混合和匹配不同的类型,我们最终会得到很多接口或基类,这些接口或基类并不是特别可重用。Sum 类型提供了一种简单、干净的方法来为此类场景组合类型。
In general, sum types allow us to store values from unrelated types in a single variable. As in the visitor pattern example, an object-oriented alternative would be to use a common base class or interface, but that doesn’t scale as well. If we mix and match different types in different places of our application, we end up with a lot of interfaces or base classes that aren’t particularly reusable. Sum types provide a simple, clean way to compose types for such scenarios.
以下语句声明了哪种类型?
让 x: [数字, 字符串] = [42, "你好"];
- 原始类型
- 额头型
- 产品类型
- 金额和产品类型
What kind of type does the following statement declare?
let x: [number, string] = [42, "Hello"];
- A primitive type
- A sum type
- A product type
- Both a sum and a product type
以下语句声明了哪种类型?
让 y: 数字 | string = "你好";
- 原始类型
- 额头型
- 产品类型
- 金额和产品类型
What kind of type does the following statement declare?
let y: number | string = "Hello";
- A primitive type
- A sum type
- A product type
- Both a sum and a product type
给定 anenum Two { A, B }和 an enum Three { C, D, E },元组类型[Two, Three]有多少个可能的值?
- 2个
- 5个
- 6个
- 8个
Given an enum Two { A, B } and an enum Three { C, D, E }, how many possible values does the tuple type [Two, Three] have?
- 2
- 5
- 6
- 8
给定 anenum Two { A, B }和 an enum Three { C, D, E },该类型有多少个可能的值Two | Three?
- 2个
- 5个
- 6个
- 8个
Given an enum Two { A, B } and an enum Three { C, D, E }, how many possible values does the type Two | Three have?
- 2
- 5
- 6
- 8
在本章中,我们介绍了通过组合现有类型来创建新类型的各种方法。在第 4 章中,我们将看到如何通过依赖类型系统来编码含义和限制类型的允许值范围来提高程序的安全性。我们还将了解如何添加和删除类型信息,以及如何将其应用于序列化等场景。
In this chapter, we covered various ways to create new types by combining existing types. In chapter 4, we’ll see how we can increase the safety of our program by relying on the type system to encode meaning and restricting the range of allowed values for our types. We’ll also see how we can add and remove type information and how this can be applied to scenarios such as serialization.
c—命名坐标的三个分量是首选方法。
c—Naming the three components of the coordinates is the preferred approach.
c—枚举在这种情况下是合适的。根据现有要求,不需要课程。
c—An enum is appropriate in this case. With existing requirements, classes aren’t needed.
d——要么是内置求和类型,要么Optional是有效的返回类型,因为两者都可以表示没有值
d—Either a built-in sum type or Optional is a valid return type, as both can represent the absence of a value
d—最好是区分联合类型(number | number无法区分该值是否表示错误。)
d—A discriminate union type is best (number | number wouldn’t be able to distinguish whether the value represents an error.)
这是一个可能的实现:
函数访问<T1, T2, T3, U1, U2, U3>( 变体:变体<T1,T2,T3>, func1: (值: T1) => U1, func2: (值: T2) => U2, func3:(值:T3)=> U3 ): 变体<U1, U2, U3> { 开关(变体索引){ 案例 0: 返回 Variant.make1(func1(<T1>variant.value)); 情况1: 返回 Variant.make2(func2(<T2>variant.value)); 案例 2: 返回 Variant.make3(func3(<T3>variant.value)); 默认值:抛出新的错误(); } }
Here is a possible implementation:
function visit<T1, T2, T3, U1, U2, U3>( variant: Variant<T1, T2, T3>, func1: (value: T1) => U1, func2: (value: T2) => U2, func3: (value: T3) => U3 ): Variant<U1, U2, U3> { switch (variant.index) { case 0: return Variant.make1(func1(<T1>variant.value)); case 1: return Variant.make2(func2(<T2>variant.value)); case 2: return Variant.make3(func3(<T3>variant.value)); default: throw new Error(); } }
c—元组是产品类型。
c—Tuples are product types.
b—这是一个 TypeScript 和类型。
b—This is a TypeScript sum type.
c—因为元组是产品类型,所以我们将两个枚举 ( 2x 3) 的可能值相乘。
c—Because tuples are product types, we multiply the possible values of the two enums (2 x 3).
2b—因为这是求和类型,所以我们将两个枚举 ( + )的可能值相加3。
b—Because this is a sum type, we add the possible values of the two enums (2 + 3).
本章涵盖
This chapter covers
现在我们知道了如何使用我们的编程语言提供的基本类型以及如何组合它们来创建新类型,让我们看看如何通过使用类型使我们的程序更安全。更安全,我的意思是减少错误的机会。
Now that we know how to use the basic types provided by our programming language and how to compose them to create new types, let’s look at how we can make our programs safer by using types. By safer, I mean reducing the opportunity for bugs.
有几种方法可以通过创建编码附加信息的新类型来实现这一点:意义和保证。前者(我们将在第一部分介绍)消除了我们误解值的机会,例如将一英里误认为一公里。0后者允许我们在类型系统中编码保证,例如“此类型的实例永远不会小于 ”。这两种技术都使我们的代码更安全,因为我们从可能值集中消除了无效值 由类型表示,并尽快避免误解,最好是在编译时,或者如果是在运行时,则在我们实例化我们的类型时尽快。当我们有一个类型的实例时,从那时起我们就知道它代表什么并且它是一个有效值。
There are a couple of ways to achieve this by creating new types that encode additional information: meanings and guarantees. The former, which we’ll cover in the first section, removes the opportunity for us to misinterpret a value, such as mistaking a mile for a kilometer. The latter allows us to encode guarantees such as “an instance of this type will never be less than 0” in the type system. Both techniques make our code safer, as we eliminate invalid values from the set of possible values represented by a type and avoid misunderstandings as soon as we can, preferably at compile time or as soon as we instantiate our types if at run time. When we have an instance of one of our types, from then on we know what it represents and that it is a valid value.
因为我们正在讨论类型安全,所以我们还将研究如何手动添加和隐藏类型检查器的信息。如果我们以某种方式比类型检查器知道的更多,我们可以告诉它信任我们并将我们的信息传递给它。另一方面,如果类型检查器知道太多并最终阻碍了我们的工作,我们可以让它“忘记”一些类型信息,以安全为代价为我们提供更大的灵活性。这些技术不能轻易使用,因为它们将正确类型检查的责任从类型检查器转移到我们作为开发人员,但正如我们将看到的,有一些合法的场景需要这些技术。
Because we’re discussing type safety, we’ll also look at how we can add and hide information from the type checker manually. If we somehow know more than the type checker does, we can tell it to trust us and pass our information down to it. On the other hand, if the type checker knows too much and ends up impeding our work, we can make it “forget” some of the typing information, giving us more flexibility at the cost of safety. These techniques are not to be used lightly, as they move the responsibility of proper type checking from the type checker to us as developers, but as we’ll see, there are some legitimate scenarios in which these techniques are desired.
在本节中,我们将看到当代码的两个不同部分(通常由不同的开发人员编写)做出不兼容的假设时,如何使用基本类型来表示值并隐式假设这些值表示什么会导致问题(图 4.1 )。
In this section, we’ll see how using basic types to represent values and implicitly assuming what those values represent can cause problems when two different parts of the code, often written by different developers, make incompatible assumptions (figure 4.1).
我们可以依靠类型系统通过定义类型来描述它们来明确这些假设,在这种情况下,类型检查器可以检测到不兼容性并在任何不良事件发生之前发出信号。
We can rely on the type system to make those assumptions explicit by defining types to describe them, in which case the type checker can detect incompatibilities and signal them before anything bad happens.
假设我们有一个函数addToBill(),它的参数是 a number。该功能应该将商品的价格添加到账单中。因为参数是 类型的number,我们可以将城市之间的距离以英里为单位传递给它,也表示为 a number。我们最终将里程数加到总价格中,类型检查器不会怀疑任何事情!
Let’s say we have a function addToBill() that takes as its argument a number. The function is supposed to add the price of an item to a bill. Because the argument is of type number, we could pass it a distance between cities in miles, also represented as a number. We end up adding miles to a price total, and the type checker doesn’t suspect anything!
另一方面,如果我们让我们的addToBill()函数接受 type 的参数Currency并且我们的城市之间的距离表示为 type Miles,代码将无法编译(图 4.2)。
On the other hand, if we make our addToBill() function take an argument of type Currency and our distance between cities is represented as a type Miles, the code will not compile (figure 4.2).
火星气候轨道飞行器解体是因为洛克希德公司开发的组件使用不同的动量测量单位(磅力秒)而不是 NASA 开发的组件,后者使用该测量值(公制单位)。让我们想象一下代码如何查找这两个组件。该trajectory-Correction()函数以牛顿秒或 Ns(动量的公制单位)为单位消耗测量值,而该provideMomentum()函数以磅力秒或 lbfs 为单位产生测量值,如下一个清单所示。
The Mars Climate Orbiter disintegrated because a component developed by Lockheed used a different unit of measure (pound-force seconds) for momentum than a component developed by NASA, which consumed that measure (in metric units). Let’s imagine how the code looked for the two components. The trajectory-Correction() function consumes a measurement as Newton-seconds, or Ns (the metric unit for momentum), whereas the provideMomentum() function produces a measure in pound-force seconds, or lbfs, as shown in the next listing.
function trajectoryCorrection(momentum: number) { 1
if (momentum < 2 /* Ns */) { 2
瓦解();
}
/* ... */
}
函数 provideMomentum() {
trajectoryCorrection(1.5 /* lbfs */); 3
}function trajectoryCorrection(momentum: number) { 1
if (momentum < 2 /* Ns */) { 2
disintegrate();
}
/* ... */
}
function provideMomentum() {
trajectoryCorrection(1.5 /* lbfs */); 3
}
转换为公制,1 lbfs 等于 4.448222 Ns。从功能的角度来看provide-Momentum(),提供的值很好,因为 1.5 lbfs 比 6 Ns 多。这远远超过了 2 Ns 的下限。什么地方出了错?这种情况下的主要问题是,两个分量都将动量视为一个数字,隐含地假定了测量动量的单位。trajectoryCorrection()将动量解释为 1 Ns,小于 2 Ns 下限,不恰当地触发了解体。
Converting to metric, 1 lbfs equals 4.448222 Ns. From the perspective of the provide-Momentum() function, the value provided is good, because 1.5 lbfs is more than 6 Ns. That’s way more than the 2 Ns lower limit. What went wrong? The main issue in this case is that both components treated momentum as a number, implicitly assuming the unit in which it was measured. trajectoryCorrection() interpreted the momentum as 1 Ns, less than the 2 Ns lower limit, and inappropriately triggered the disintegration.
让我们看看我们是否可以利用类型系统来防止这种灾难性的误解。让我们通过在清单 4.2Lbfs中定义类型和类型来明确度量单位。两种类型都包含一个数字,因为实际度量仍然是一个值。我们将为每种类型使用唯一的符号,因为 TypeScript 认为具有相同形状的类型是兼容的,正如我们将在讨论子类型时看到的那样。独特的符号技巧使得一种类型不能被隐式解释为另一种类型。并非所有语言都需要这个额外的唯一符号成员。我们将在第 7 章解释这个技巧;现在,我们将专注于定义的新类型。 Ns
Let’s see whether we can leverage the type system to prevent such catastrophic misunderstandings. Let’s make the unit of measure explicit by defining a Lbfs type and a Ns type in listing 4.2. Both types wrap a number, as the actual measure is still a value. We will use a unique symbol for each type because TypeScript considers types to be compatible if they have the same shape, as we will see when we discuss subtyping. The unique symbol trick makes it so that one type can’t be implicitly interpreted as the other. Not all languages require this additional unique symbol member. We’ll explain this trick in chapter 7; for now, we’ll focus on the new types defined.
声明 const NsType:唯一符号; 1个
类 Ns {
只读值:数字; 2
[NsType]: void; 1个
构造函数(值:数字){
this.value = 值;
}
}
声明 const LbfsType:唯一符号;
类 Lbfs {
只读值:数字; 3
[LbfsType]: void; 3个
构造函数(值:数字){
this.value = 值;
}
}declare const NsType: unique symbol; 1
class Ns {
readonly value: number; 2
[NsType]: void; 1
constructor(value: number) {
this.value = value;
}
}
declare const LbfsType: unique symbol;
class Lbfs {
readonly value: number; 3
[LbfsType]: void; 3
constructor(value: number) {
this.value = value;
}
}
现在我们有了两个不同的类型,我们可以很容易地实现它们之间的转换,因为我们知道比率。让我们看看下面的清单,看看从 lbfs 到 Ns 的转换,这是我们更新trajectoryCorrection()代码中需要的。
Now that we have our two separate types, we can easily implement a conversion between them because we know the ratio. Let’s look at the following listing to see a conversion from lbfs to Ns, which we need in our update trajectoryCorrection() code.
函数 lbfsToNs(lbfs: Lbfs): Ns {
返回新的 Ns(lbfs.value * 4.448222); 1
}function lbfsToNs(lbfs: Lbfs): Ns {
return new Ns(lbfs.value * 4.448222); 1
}
回到火星气候轨道器,我们可以重新实现这两个函数以使用新类型。trajectoryCorrection()期望 Ns 动量(如果该值小于 2 Ns,仍会分解),并且provideMomentum()仍会产生 lbfs 值。但是现在我们不能简单地获取由产生的值provideMomentum()并将其传递给trajectoryCorrection(),因为返回值和函数参数具有不同的类型。我们必须使用我们的lbfsToNs()函数显式地从一个转换为另一个,如以下清单所示。
Going back to the Mars Climate Orbiter, we can reimplement the two functions to use the new types. trajectoryCorrection() expects a Ns momentum (and will still disintegrate if the value is less than 2 Ns), and provideMomentum() still produces values as lbfs. But now we can’t simply take the value produced by provideMomentum() and pass it to trajectoryCorrection(), because the returned value and the function argument have different types. We have to explicitly convert from one to the other, using our lbfsToNs() function, as the following listing shows.
function trajectoryCorrection(momentum: Ns ) { 1
if ( momentum.value < new Ns(2).value ) { 1
瓦解();
}
/* ... */
}
函数 provideMomentum() {
trajectoryCorrection( lbfsToNs(new Lbfs(1.5)) ); 2
}function trajectoryCorrection(momentum: Ns) { 1
if (momentum.value < new Ns(2).value) { 1
disintegrate();
}
/* ... */
}
function provideMomentum() {
trajectoryCorrection(lbfsToNs(new Lbfs(1.5))); 2
}
如果我们省略转换lbfsToNs(),代码将无法编译,并且会出现以下错误:Argument of type 'lbfs' is not assignable to parameter of type 'Ns'. Property '[NsType]' is missing in type 'lbfs'.
If we omitted the conversion lbfsToNs(), the code would simply not compile, and we would get the following error: Argument of type 'lbfs' is not assignable to parameter of type 'Ns'. Property '[NsType]' is missing in type 'lbfs'.
让我们回顾一下发生了什么:我们从两个都操纵动量值的组件开始,但即使它们在处理这些值时使用不同的单位,它们都将这些值简单地表示为number. 为了避免误解,我们创建了几个新类型,一个代表每个度量单位,这有效地消除了误解的余地。如果一个组件显式地处理Ns,它就不会意外地消耗一个Lbfs值。
Let’s review what happened: we started with two components that both manipulated momentum values, but even though they used different units when handling those values, they both represented the values simply as number. To avoid misinterpretations, we created a couple of new types, one to represent each unit of measure, which effectively left no room for misinterpretation. If a component explicitly deals with Ns, it can’t accidentally consume a Lbfs value.
另请注意,在我们的第一个示例 ( ) 中作为注释出现在代码中的假设1.5 /* lbfs */在我们的最终实现 ( new Lbfs(1.5)) 中变成了代码。
Also note that the assumptions that showed up in the code as comments in our first example (1.5 /* lbfs */) became code in our final implementation (new Lbfs(1.5)).
与设计模式捕获高度可靠和有效的可重用软件设计的方式相同,反模式是常见的设计,当存在更好的替代方案时,这些设计无效且适得其反。前面的示例是一个名为primitive obsession的著名反模式的实例。当我们依赖基本类型来表示一切时,就会出现对原始的痴迷:邮政编码是 a number,电话号码是 a string,等等。
In the same way that design patterns capture reusable software designs that are highly reliable and effective, antipatterns are common designs that are ineffective and counterproductive when a better alternative exists. The preceding example is an instance of a well-known antipattern called primitive obsession. Primitive obsession turns up when we rely on basic types to represent everything: a postal code is a number, a phone number is a string, and so on.
如果我们落入这个陷阱,就会为我们在本节中看到的错误留出很大空间。那是因为值的含义没有在类型系统中明确捕获。如果我消耗一个作为 a 给出的动量值number,我,开发人员,隐含地假设它是一个牛顿秒值。类型检查器没有足够的信息来检测两个开发人员何时做出不兼容的假设。当此假设被显式捕获为类型声明时,并且我使用作为实例给出的动量值Ns时,类型检查器可以验证其他人何时试图给我一个Lbfs实例而不是允许代码编译。
If we fall into this trap, we leave a lot of room for errors like the one we saw in this section. That’s because the meaning of the values is not explicitly captured in the type system. If I consume a momentum value given as a number, I, the developer, implicitly assume that it is a Newton-second value. The type checker does not have enough information to detect when two developers make incompatible assumptions. When this assumption is explicitly captured as a type declaration, and I consume a momentum value given as a Ns instance, the type checker can verify when someone else is attempting to give me a Lbfs instance instead and not allow the code to compile.
尽管邮政编码是一个数字,但这并不意味着我们应该将它存储为 type 的值number。我们永远不应该将动量解释为邮政编码。
Even though a postal code is a number, that doesn’t mean we should store it as a value of type number. We should never interpret momentum as a postal code.
如果您表示的实体是简单的值,例如物理测量值和邮政编码,请考虑将它们定义为新类型,即使这些类型只是简单地包含一个数字或字符串。这种做法为类型系统提供了更多信息来分析我们的代码,并消除了由不兼容假设引起的一整类错误,更不用说它使代码更具可读性。作为对比,将 的第一个定义(trajectoryCorrection()即 )与第二个定义(即 )进行比较。第二个向代码的读者提供更多关于其合同是什么的信息。(预期动量在.trajectory-Correction(momentum: number)trajectory--Correction(momentum: Ns)Ns)
If the entities you represent are simple values, such as physical measurements and postal codes, consider defining them as new types, even if these types simply wrap a number or a string. This practice gives the type system more information to work with in analyzing our code and eliminates a whole class of errors caused by incompatible assumptions, not to mention that it makes the code more readable. For contrast, compare the first definition of trajectoryCorrection(), which is trajectory-Correction(momentum: number), with the second one, which is trajectory--Correction(momentum: Ns). The second one gives more information to readers of the code as to what its contract is. (Expected momentum is in Ns.)
到目前为止,我们已经了解了如何将基本类型包装到其他类型中以编码更多信息。现在让我们继续看看如何通过限制给定类型的允许值范围来提供更高的安全性。
So far, we’ve seen how we can wrap primitive types into other types to encode more information. Now let’s move on to see how we can provide even more safety by restricting the range of allowed values for a given type.
表示重量测量的最安全方法是什么?
- 作为一个number
- 作为一个string
- 作为自定义Kilograms类型
- 作为自定义Weight类型
What is the safest way to represent a weight measurement?
- As a number
- As a string
- As a custom Kilograms type
- As a custom Weight type
在第 3 章中,我们讨论了组合以及如何采用基本类型并将它们组合起来以表示更复杂的概念,例如将 2D 平面上的点表示为一对数值,X 和 Y 坐标各一个。现在让我们看看当开箱即用的基本类型允许比我们需要的更多的值时我们可以做什么。
In chapter 3, we talked about composition and how to take basic types and combine them to represent more complex concepts, such as representing a point on a 2D plane as a pair of number values, one for each of the X and Y coordinates. Now let’s look at what we can do when the basic types we get out of the box allow for more values than we need.
让我们以测量温度为例。我们将避免原始的痴迷并声明一个Celsius类型以明确我们期望温度具有哪种测量单位。这种类型也将简单地包装一个数字。
Let’s take, as an example, a measure of temperature. We’re going to avoid primitive obsession and declare a Celsius type to make it clear which unit of measure we expect the temperature to have. This type will also simply wrap a number.
不过,我们还有一个额外的限制条件:我们的温度绝不能低于绝对零值,即 –273.15 摄氏度。一种选择是在我们使用这种类型的实例时检查该值是否有效。但是,此选项为错误留下了空间:我们总是添加检查,但团队中的新开发人员不知道该模式并且错过了检查。确保我们永远不会得到无效值不是更好吗?
We have an additional constraint, though: we should never have a temperature less than absolute zero, which is –273.15 degrees Celsius. One option is to check whenever we use an instance of this type that the value is a valid one. This option leaves room for error, though: we always add the check, but a new developer on the team doesn’t know the pattern and misses checking. Wouldn’t it be better to make sure that we can never get an invalid value?
我们可以通过两种方式做到这一点:通过构造函数或通过工厂。
We can do this in two ways: via the constructor or via a factory.
我们可以在构造函数中实现约束,并以我们在查看整数溢出时看到的两种方式之一来处理太小的值。一种选择是在值无效时抛出异常并禁止创建对象。
We can implement the constraint in the constructor and handle a value that’s too small in one of the two ways we saw when we looked at integer overflow. One option is to throw an exception when the value is invalid and disallow creation of the object.
声明 const celsiusType:唯一符号;
类摄氏{
只读值:数字; 1个
[摄氏类型]: void;
构造函数(值:数字){
如果(值 < -273.15)抛出新错误(); 2个
this.value = 值;
}
}declare const celsiusType: unique symbol;
class Celsius {
readonly value: number; 1
[celsiusType]: void;
constructor(value: number) {
if (value < -273.15) throw new Error(); 2
this.value = value;
}
}
我们通过 make 确保值在构造后保持有效readonly。另一种选择是将其设为私有并使用 getter 访问它(以便可以检索但不能设置值)。
We ensure that the value stays valid after construction by making it readonly. Another option would be to make it private and access it with a getter (so that the value can be retrieved but not set).
我们还可以实现我们的构造函数以将值强制为有效值:任何小于-273.15becomes 的值-273.15。
We can also implement our constructor to coerce the value to be a valid one: anything less than -273.15 becomes -273.15.
声明 const celsiusType:唯一符号;
类摄氏{
只读值:数字;
[摄氏类型]: void;
构造函数(值:数字){
如果(值<-273.15)值=-273.15; 1个
this.value = 值;
}
}declare const celsiusType: unique symbol;
class Celsius {
readonly value: number;
[celsiusType]: void;
constructor(value: number) {
if (value < -273.15) value = -273.15; 1
this.value = value;
}
}
这两种方法中的任何一种都有效,具体取决于场景。我们也可以改用工厂函数。工厂是一个类或函数,其主要工作是创建另一个对象。
Either of the two approaches is valid, depending on the scenario. We can also use a factory function instead. A factory is a class or function whose main job is to create another object.
当我们不想抛出异常,而是返回undefined或不是温度的其他值并表示无法创建有效实例时,工厂很有用。构造函数不能这样做,因为它不返回:它要么完成实例初始化,要么抛出异常。使用工厂的另一个原因是当构造和验证对象所需的逻辑很复杂时,在这种情况下,在构造函数之外实现它可能是有意义的。根据经验,构造函数不应该做繁重的工作——只需初始化对象成员即可。
A factory is useful when we don’t want to throw an exception, but to return undefined or some other value that is not a temperature and represents failure to create a valid instance. A constructor can’t do this because it doesn’t return: it either finishes initializing its instance or throws. Another reason to use a factory is when the logic required to construct and validate an object is complex, in which case it might make sense to implement it outside the constructor. As a rule of thumb, constructors shouldn’t do heavy lifting—just get the object members initialized.
让我们看看下面清单中工厂的实现是如何工作的。我们将使构造函数成为私有的,这样只有工厂方法才能调用它。工厂将是我们班级的静态方法。它将返回一个Celsius实例或undefined.
Let’s look at how an implementation of a factory works in the following listing. We will make the constructor private so that only the factory method can call it. The factory will be a static method on our class. It will return either a Celsius instance or undefined.
声明 const celsiusType:唯一符号;
类摄氏{
只读值:数字;
[摄氏类型]: void;
私有构造函数(值:数字){ 1
this.value = 值;
}
静态 makeCelsius(值:数字):摄氏 | 未定义 { 2
如果(值 < -273.15)返回未定义; 3个
返回新的摄氏度(值);
}
}declare const celsiusType: unique symbol;
class Celsius {
readonly value: number;
[celsiusType]: void;
private constructor(value: number) { 1
this.value = value;
}
static makeCelsius(value: number): Celsius | undefined { 2
if (value < -273.15) return undefined; 3
return new Celsius(value);
}
}
在所有这些情况下,我们都有额外的保证,如果我们有一个 的实例Celsius,它的值永远不会小于-273.15。在创建该类型的实例时执行检查并确保不能以其他方式创建该类型的优点是,只要您看到该类型的实例被传递,就可以保证您获得有效值。
In all these cases, we have the additional guarantee that if we have an instance of Celsius, its value will never be less than -273.15. The advantage of performing the check when an instance of the type is created and ensuring that the type can’t be created in other ways is that you are guaranteed a valid value whenever you see an instance of the type being passed around.
我们不是在使用实例时检查实例是否有效,这通常意味着在多个地方执行检查,而是只执行一次检查,并使该类型的无效对象不可能存在。
Instead of checking whether the instance is valid when using it, which usually means performing the check in multiple places, we perform the check just once and make it impossible for an invalid object of the type to exist.
Celsius当然,这种技术超越了简单的值包装器,例如 。我们可以确保Date根据年、月、日创建的对象是有效的,并且不允许像 6 月 31 日这样的日期。在许多情况下,我们可以使用的基本类型不允许我们直接施加我们想要的限制,在这种情况下,我们可以创建封装额外约束的类型,并保证它们不会存在无效值。
This technique goes beyond simple value wrappers like Celsius, of course. We can ensure that a Date object created from a year, a month, and a day is valid and disallow dates like June 31. There are many cases in which the basic types at our disposal don’t allow us to impose the restrictions we want directly, in which case we can create types that encapsulate additional constraints and provide the guarantee that they can’t exist with invalid values.
接下来,让我们看看如何在整个代码中添加和隐藏键入信息,以及这种做法何时有用。
Next, let’s look at how we can add and hide typing information throughout our code and when this practice is useful.
实现一个Percentage表示 0 到 100 之间的值的类型。小于 0 的值应变为 0,大于 100 的值应变为 100。
Implement a Percentage type that represents a value between 0 and 100. Values smaller than 0 should become 0, and values larger than 100 should become 100.
尽管类型检查具有强大的理论基础,但所有编程语言都提供了捷径,使我们能够绕过类型检查并告诉编译器将值视为特定类型。我们实际上是在说:“相信我们;我们比你更了解这种类型的什么。” 这称为类型转换——您可能以前听过这个术语。
Although type checking has strong theoretical foundations, all programming languages provide shortcuts that allow us to bypass the type checks and tell the compiler to treat a value as a certain type. We are effectively saying, “Trust us; we know what this type is better than you do.” This is called a type cast—a term you might have heard before.
类型转换将表达式的类型转换为另一种类型。每种编程语言都有自己的规则,关于哪些转换有效,哪些转换无效,哪些可以由编译器自动完成,哪些必须使用额外的代码来完成(图 4.3 )。
A type cast converts the type of an expression to another type. Each programming language has its own rules about which conversions are valid and which are not, which can be done automatically by the compiler, and which must be done with additional code (figure 4.3).
显式类型转换是一种允许我们告诉编译器将值视为具有特定类型的类型的转换。在 TypeScript 中,我们通过在值前NewType添加或在值后 添加来进行强制转换。<NewType>as NewType
An explicit type cast is a cast that allows us to tell the compiler to treat a value as though it had a certain type. In TypeScript, we do a cast to NewType by adding <NewType> in front of the value or by adding as NewType after the value.
如果使用不当,这种技术可能会很危险:如果我们绕过类型检查器,如果我们试图将某个值用作不是它的值,则会出现运行时错误。例如,我可以将我的Bike,我可以ride(),转换为SportsCar,但我仍然无法drive()这样做,如以下清单所示。
This technique can be dangerous when misused: if we bypass the type checker, we get a run-time error if we attempt to use a value as something it is not. I can cast my Bike, which I can ride(), to a SportsCar, for example, but I still won’t be able to drive() it, as the following listing shows.
类自行车{
ride(): void { /* ... */ }
}
跑车类 {
驱动器():void { /* ... */ }
}
让我的自行车:自行车=新自行车(); 1
1
myBike.ride(); 1个
让 myPretendSportsCar: SportsCar = <SportsCar><unknown>myBike; 2个
myPretendSportsCar.drive(); 3个class Bike {
ride(): void { /* ... */ }
}
class SportsCar {
drive(): void { /* ... */ }
}
let myBike: Bike = new Bike(); 1
1
myBike.ride(); 1
let myPretendSportsCar: SportsCar = <SportsCar><unknown>myBike; 2
myPretendSportsCar.drive(); 3
在这里,我们可以告诉类型检查器让我们假装我们有一个SportsCar,但这并不意味着我们实际上有一个。调用drive导致抛出以下异常:TypeError: myPretendSportsCar.drive is not a function。
Here, we can tell the type checker to let us pretend that we have a SportsCar, but that doesn’t mean we actually have one. Calling drive results in the following exception being thrown: TypeError: myPretendSportsCar.drive is not a function.
我们必须myBike先转换为unknown类型,然后再转换为 a SportsCar,因为 TypeScript 编译器意识到 theBike和SportsCar类型不重叠。(其中一种类型的有效值永远不可能是另一种类型的有效值。)所以简单地调用<SportsCar>myBike仍然会导致错误。相反,我们首先说<unknown>myBike,它告诉编译器忘记 的类型myBike。然后我们可以说,“相信我们;这是一个SportsCar。但正如我们所见,这仍然会导致运行时错误。在其他语言中,它可能会导致崩溃。一般来说,这种情况是无效的。那么这什么时候有用呢?
We had to cast myBike first to the unknown type and then to a SportsCar because the TypeScript compiler realizes that the Bike and SportsCar types don’t overlap. (A valid value of one of the types can never be a valid value of the other.) So simply calling <SportsCar>myBike still causes an error. Instead, we first say <unknown>myBike, which tells the compiler to forget the type of myBike. Then we can say, “Trust us; it’s a SportsCar.” But as we saw, this still causes a run-time error. In other languages, it can cause a crash. In general, such a situation is not valid. So when would this be useful?
有时,我们知道的比类型检查器还多。让我们回顾一下第 3 章Either的实现。它存储或类型的值,并且有一个标志跟踪该值是否为,如下一个清单所示。 TLeftTRightbooleanTLeft
Sometimes, we know more than the type checker. Let’s revisit the Either implementation from chapter 3. It stores a value of TLeft or TRight type, and a boolean flag keeps track of whether the value is TLeft, as shown in the next listing.
类 Either<TLeft, TRight> {
私有只读值:TLeft | 对; 1
个私有只读左:布尔值; 2个
私有构造函数(值:TLeft | TRight,左:布尔值){
this.value = 值;
this.left = 左;
}
isLeft(): 布尔值 {
返回 this.left;
}
getLeft(): TLeft {
如果(!this.isLeft())抛出新的错误(); 3
3
返回 <TLeft>this.value; 3个
}
isRight(): 布尔值 {
返回 !this.left;
}
getRight(): 正确{
如果(!this.isRight())抛出新的错误();
返回 <TRight>this.value;
}
静态 makeLeft<TLeft, TRight>(值: TLeft) {
返回新的 Either<TLeft, TRight>(value, true); 4个
}
静态 makeRight<TLeft, TRight>(值: TRight) {
返回新的 Either<TLeft, TRight>(value, false); 4个
}
}class Either<TLeft, TRight> {
private readonly value: TLeft | TRight; 1
private readonly left: boolean; 2
private constructor(value: TLeft | TRight, left: boolean) {
this.value = value;
this.left = left;
}
isLeft(): boolean {
return this.left;
}
getLeft(): TLeft {
if (!this.isLeft()) throw new Error(); 3
3
return <TLeft>this.value; 3
}
isRight(): boolean {
return !this.left;
}
getRight(): TRight {
if (!this.isRight()) throw new Error();
return <TRight>this.value;
}
static makeLeft<TLeft, TRight>(value: TLeft) {
return new Either<TLeft, TRight>(value, true); 4
}
static makeRight<TLeft, TRight>(value: TRight) {
return new Either<TLeft, TRight>(value, false); 4
}
}
这允许我们将两种类型组合成一个总和类型,该类型可以表示其中任何一个的值。但是,如果我们仔细观察,我们存储的值的类型是TLeft | TRight。在我们分配它之后,类型检查器不再知道value我们实际存储的是 aTLeft还是 a TRight。从现在开始,它将考虑value成为两者之一。这是我们存储值时想要的,但在某些时候,我们想使用它。
This allows us to combine two types into a sum type that can represent a value from either of them. If we look closely, though, the value we are storing has type TLeft | TRight. After we assign it, the type checker no longer knows whether the actual value we stored was a TLeft or a TRight. From now on, it will consider value to be either of the two. This is what we want while storing the value, but at some point, we would like to use it.
编译器不允许我们将 type 的值传递TLeft | TRight给需要值的函数TLeft,因为如果我们的值实际上是TRight,我们就会有麻烦了。如果我们有一个三角形或一个正方形,我们不一定能通过三角形槽。有一个三角形穿过它会起作用。但是如果我们有一个正方形(图 4.4)呢?
The compiler will not allow us to pass a value of type TLeft | TRight to a function that expects a TLeft value, because if our value is in fact TRight, we are going to be in trouble. If we have a triangle or a square, we can’t necessarily pass that through a triangular slot. It would work to have a triangle to pass through it. But what if we have a square (figure 4.4)?
尝试做这样的事情会导致编译器错误,这很好。但是我们知道类型检查器不知道的事情:我们从设置值时就知道它是来自 aTLeft还是 a TRight。如果我们使用 创建我们的对象makeLeft(),我们设置left为true。如果我们使用 创建我们的对象makeRight(),我们设置left为false,如下一个清单所示。即使类型检查器忘记了,我们也会跟踪这个事实。
Trying to do something like this results in a compiler error, which is good. But we know something the type checker doesn’t: we know from when we set the value whether it came from a TLeft or a TRight. If we created our object by using makeLeft(), we set left to true. If we created our object by using makeRight(), we set left to false, as shown in the next listing. We are keeping track of this fact even if the type checker forgets.
类 Either<TLeft, TRight> {
私有只读值:TLeft | 对;
私有只读左:布尔值; 1个
私有构造函数(值:TLeft | TRight,左:布尔值){
this.value = 值;
this.left = 左; 2个
}
/* ... */
静态 makeLeft<TLeft, TRight>(值: TLeft) {
返回新的 Either<TLeft, TRight>(value, true ); 3个
}
静态 makeRight<TLeft, TRight>(值: TRight) {
返回新的 Either<TLeft, TRight>(value, false ); 3个
}
}class Either<TLeft, TRight> {
private readonly value: TLeft | TRight;
private readonly left: boolean; 1
private constructor(value: TLeft | TRight, left: boolean) {
this.value = value;
this.left = left; 2
}
/* ... */
static makeLeft<TLeft, TRight>(value: TLeft) {
return new Either<TLeft, TRight>(value, true); 3
}
static makeRight<TLeft, TRight>(value: TRight) {
return new Either<TLeft, TRight>(value, false); 3
}
}
当我们要将值取出来时,作为调用者,我们有责任首先检查该值是两种类型中的哪一种。如果我们有一个Either<Triangle, Square>并且想要一个Triangle,我们首先调用isLeft()。如果true返回,我们调用getLeft()并以 结束Triangle,如以下清单所示。
When we want to take the value out, as a caller, it is our responsibility to first check which of the two types the value is. If we have an Either<Triangle, Square> and want a Triangle, we start by calling isLeft(). If true is returned, we call getLeft() and end up with a Triangle, as the following listing shows.
声明 const triangleType:唯一符号;
三角形类 { 1
[三角形类型]: void;
/* ... */
}
声明 const squareType:唯一符号;
类广场{
[方型]: void; 1个
/* ... */
}
函数槽(三角形:三角形){
/* ... */
}
让 myTriangle: Either<Triangle,Square>
= Either.makeLeft(新三角形()); 2个
如果 (myTriangle.isLeft())
插槽(myTriangle.getLeft()); 3个declare const triangleType: unique symbol;
class Triangle { 1
[triangleType]: void;
/* ... */
}
declare const squareType: unique symbol;
class Square {
[squareType]: void; 1
/* ... */
}
function slot(triangle: Triangle) {
/* ... */
}
let myTriangle: Either<Triangle,Square>
= Either.makeLeft(new Triangle()); 2
if (myTriangle.isLeft())
slot(myTriangle.getLeft()); 3
在内部,我们的getLeft()实现执行它需要的任何检查(在这种情况下通过检查 is this.isLeft())true并处理我们想要的无效调用(在这种情况下通过抛出Error)。当所有这些都不碍事时,它将值转换为类型。当我们分配了它,所以现在我们提醒它,如下面的代码所示,因为我们一直在跟踪left.
Internally, our getLeft() implementation performs whatever checks it needs (in this case by checking that this.isLeft() is true) and handles an invalid call however we want (in this case by throwing Error). When all that is out of the way, it casts the value to the type. The type checker forgot which type the value was when we assigned it, so now we remind it, as shown in the following code, as we were keeping track of the type in left.
类 Either<TLeft, TRight> {
私有只读值:TLeft | 对;
私有只读左:布尔值;
/* ... */
isLeft(): 布尔值 {
返回 this.left; 1个
}
getLeft(): TLeft {
如果(!this.isLeft())抛出新的错误(); 2个
返回 <TLeft>this.value; 3个
}
/* ... */
}class Either<TLeft, TRight> {
private readonly value: TLeft | TRight;
private readonly left: boolean;
/* ... */
isLeft(): boolean {
return this.left; 1
}
getLeft(): TLeft {
if (!this.isLeft()) throw new Error(); 2
return <TLeft>this.value; 3
}
/* ... */
}
在这种情况下,我们不需要<unknown>强制转换:类型的值TLeft | TRight可以是 type 的有效值TLeft,因此编译器不会抱怨并且会信任我们进行强制转换。
In this case, we don’t need the <unknown> cast: a value of the type TLeft | TRight could be a valid value of type TLeft, so the compiler won’t complain and will trust us with the cast.
如果使用得当,转换功能非常强大,因为它允许我们细化值的类型。如果我们有一个Triangle | Square, 并且我们知道它是一个Triangle, 我们可以将它转换为一个Triangle, 编译器将允许我们通过一个三角形槽来适应它。
When used correctly, casting is powerful because it allows us to refine the type of a value. If we have a Triangle | Square, and we know that it is a Triangle, we can cast it to a Triangle, which the compiler will allow us to fit through a triangular slot.
事实上,大多数类型检查器会自动执行多个此类转换,而无需我们编写任何代码。
In fact, most type checkers do several such casts automatically without requiring us to write any code.
隐式类型转换,也称为强制转换,是由编译器自动执行的类型转换。它不需要编写任何代码。这样的演员表通常是安全的。相比之下,显式类型转换是我们需要用代码指定的类型转换。这种类型转换有效地绕过了类型系统的规则,我们应该小心使用它。
An implicit type cast, also known as coercion, is a type cast that is performed automatically by the compiler. It doesn’t require any code to be written. Such casts are usually safe. By contrast, an explicit type cast is a type cast that we need to specify with code. This type cast effectively bypasses the rules of the type system, and we should use it with care.
让我们看一下几种常见的类型转换(隐式和显式),看看它们有何用处。
Let’s look at a few common types of casts, both implicit and explicit, and see how they can be useful.
常见类型转换的一个示例是将从另一种类型继承的类型的对象解释为其父类型。如果我们的基类是Shape,并且我们有一个Triangle,我们总是可以在需要Trianglea 时使用 a Shape,如以下代码所示。
One example of a common type cast is interpreting an object of a type that inherits from another type as its parent type. If our base class is Shape, and we have a Triangle, we can always use a Triangle whenever a Shape is required, as shown in the following code.
类形状{
/* ... */
}
声明 const triangleType:唯一符号;
三角形类扩展形状 { 1
[三角形类型]: void;
/* ... */
}
函数 useShape(形状:形状){ 2
/* ... */
}
让我的三角形:三角形=新三角形();
使用形状(我的三角形); 3个class Shape {
/* ... */
}
declare const triangleType: unique symbol;
class Triangle extends Shape { 1
[triangleType]: void;
/* ... */
}
function useShape(shape: Shape) { 2
/* ... */
}
let myTriangle: Triangle = new Triangle();
useShape(myTriangle); 3
在 的主体内useShape(),编译器将参数视为一个Shape,即使我们传入了一个Triangle。将派生类 ( Triangle) 解释为基类 ( Shape) 称为向上转型。如果我们确定我们的 Shape 实际上是一个三角形,我们可以将它转换回Triangle,但是这个转换需要是显式的。从父类到派生类的转换称为向下转换,如下一个清单所示,大多数强类型语言不会自动执行此操作。
Inside the body of useShape(), the compiler treats the argument as a Shape, even if we passed in a Triangle. Interpreting a derived class (Triangle) as a base class (Shape) is called an upcast. If we know for sure that our Shape is actually a Triangle, we can cast it back to Triangle, but this cast needs to be explicit. Casting from a parent class to a derived class is called a downcast, shown in the next listing, and most strongly typed languages don’t do this automatically.
类形状{
/* ... */
}
声明 const triangleType:唯一符号;
类三角形扩展形状{
[三角形类型]: void;
/* ... */
}
function useShape(shape: Shape, isTriangle: boolean ) { 1
如果(是三角形){
let triangle: Triangle = <Triangle>形状; 2个
/* ... */
}
/* ... */
}
让我的三角形:三角形=新三角形();
使用形状(我的三角形,真); 3个class Shape {
/* ... */
}
declare const triangleType: unique symbol;
class Triangle extends Shape {
[triangleType]: void;
/* ... */
}
function useShape(shape: Shape, isTriangle: boolean) { 1
if (isTriangle) {
let triangle: Triangle = <Triangle>shape; 2
/* ... */
}
/* ... */
}
let myTriangle: Triangle = new Triangle();
useShape(myTriangle, true); 3
与向上转型不同,向下转型并不安全。虽然很容易从派生类中分辨出它的父类是什么,但是编译器无法自动确定给定父类,一个值可能是哪个可能的派生类。
Unlike an upcast, a downcast is not safe. Although it’s easy to tell from a derived class what its parent is, the compiler can’t automatically determine, given a parent class, which of the possible derived classes a value might be.
一些编程语言在运行时存储额外的类型信息,并包含一个is运算符,可用于查询对象的类型。当我们创建一个新对象时,它的关联类型存储在旁边,所以即使我们从编译器中向上转换了一些类型信息,在运行时我们也可以检查我们是否有一个特定类型的实例if (shape is Triangle) ...。
Some programming languages store additional type information at run time and include an is operator, which can be used to query the type of an object. When we are creating a new object, its associated type is stored alongside, so even if we upcast away some of the type information from the compiler, at run time we can check whether we have an instance of a certain type with if (shape is Triangle) ....
实现这种运行时类型信息的语言和运行时提供了一种更安全的存储和查询类型的方法,因为不存在此信息与对象不同步的风险。这是以为每个对象实例在内存中存储额外数据为代价的。
Languages and run times that implement this kind of run-time type information provide a safer way to store and query for types, as there is no risk that this information will get out of sync with the objects. This comes at the cost of storing additional data in memory for each object instance.
在第 7 章中,当我们讨论子类型时,我们将研究更复杂的向上转型并讨论方差。现在,我们将继续讨论扩大和缩小演员表。
In chapter 7, when we discuss subtyping, we will look at more complex upcasts and talk about variance. For now, we’ll move on to talk about widening and narrowing casts.
另一种常见的隐式转换是从具有固定位数的整数类型(例如 8 位无符号整数)到另一种表示具有更多位数的值的整数类型(例如 16 位无符号整数)。您可以隐式执行此操作,因为 16 位无符号整数可以表示任何 8 位无符号整数值等等。这种类型的铸件称为加宽铸件。
Another common implicit cast is from an integer type with a fixed number of bits—say, an 8-bit unsigned integer—to another integer type that represents values with more bits—say, a 16-bit unsigned integer. You can do this implicitly because a 16-bit unsigned integer can represent any 8-bit unsigned integer value and more. This type of cast is called a widening cast.
另一方面,将有符号整数转换为无符号整数是危险的,因为负数不能用无符号整数表示。类似地,将位数较多的整数转换为位数较少的整数,例如将 16 位无符号整数转换为 8 位无符号整数,仅适用于较小类型可以表示的值。
On the other hand, casting a signed integer to an unsigned integer is dangerous, as a negative number can’t be represented by an unsigned integer. Similarly, casting an integer with more bits to an integer with fewer bits, such as a 16-bit unsigned integer to an 8-bit unsigned integer, would work only for values that the smaller type can represent.
这种类型的转换称为缩小转换。某些编译器会强制您在执行缩小转换时明确表示,因为这很危险。明确有帮助,因为它清楚地表明您不是无意中这样做的。其他编译器允许缩小转换但发出警告。值不适合新值时的运行时行为type 类似于我们在第 2 章中讨论的整数溢出:根据语言的不同,我们会得到错误或值被截断以适合新类型(图 4.5)。
This type of cast is called a narrowing cast. Some compilers force you to be explicit when performing a narrowing cast because it’s dangerous. Being explicit helps, in that it makes it clear you didn’t do it unintentionally. Other compilers allow narrowing casts but issue a warning. Run-time behavior when the value doesn’t fit the new type is similar to the integer overflow that we discussed in chapter 2: depending on the language, we get an error or the value gets chopped so that it fits in the new type (figure 4.5).
不要轻易使用强制转换,因为它们绕过了类型检查器,有效地消除了类型检查给我们带来的所有好处。但是,它们是有用的工具,尤其是当我们拥有比编译器更多的信息并希望将该信息返回给编译器时。在我们告诉编译器我们所知道的之后,它可以在进一步分析中使用该信息。回到这个Triangle | Square例子,在我们告诉编译器我们的值是 a 之后,后面Triangle就没有任何值了。Square这种技术类似于 4.2 节中讨论的技术,我们在其中研究了强制约束,但在这里,我们不执行运行时检查,而是简单地告诉编译器信任我们。
Casts are not to be used lightly, as they bypass the type checker, effectively eliminating all the goodness that type checking brings us. They are useful tools, though, especially when we have more information than the compiler does and want to push that information back to the compiler. After we tell the compiler what we know, it can use that information in further analysis. Going back to the Triangle | Square example, after we tell the compiler our value is a Triangle, there can be no Square value farther on. This technique is similar to the one discussed in section 4.2, in which we looked at enforcing constraints, but here, instead of performing a run-time check, we simply tell the compiler to trust us.
在下一节中,我们将研究一些其他情况,在这些情况下,让编译器“忘记”键入信息很有用。
In the next section, we’ll look at a few other situations in which it’s useful to make the compiler “forget” typing information.
以下哪些演员表被认为是安全的?
- 向上转换
- 沮丧
- 向上转型和向下转型
- 两者都不
Which of the following casts are considered to be safe?
- Upcasts
- Downcasts
- Upcasts and downcasts
- Neither
以下哪些转换被认为是不安全的?
- 加宽铸件
- 缩小铸型
- 扩大和缩小铸件
- 两者都不
Which of the following casts are considered to be unsafe?
- Widening casts
- Narrowing casts
- Widening and narrowing casts
- Neither
隐藏类型信息的一个示例是希望拥有一个可以包含不同类型值组合的集合。如果集合只包含一种类型的值,例如一袋猫,这很容易,因为我们知道每当我们从袋子里拿出一些东西时,它都会是一只猫。如果我们也想把杂货放在袋子里,当我们拿出东西时,我们最终可能会得到一只猫或一件杂货(图 4.6)。
One example of hiding type information is wanting to have a collection that can contain a combination of values of different types. If the collection contains values of just one type, such as a bag of cats, it’s easy, because we know that whenever we pull some thing out from the bag, it’s going to be a cat. If we want to put groceries in the bag too, when we pull something out, we might end up with either a cat or a grocery item (figure 4.6).
具有相同类型物品的集合,例如我们的猫袋,也称为同质集合。因为所有项目都有相同的类型,所以我们不需要隐藏它们的类型信息。不同类型项目的集合也称为异构集合。在这种情况下,我们需要隐藏一些类型信息来声明这样一个集合。
A collection with items of the same type, like our bag of cats, is also called a homogenous collection. Because all items have the same type, we don’t need to hide their type information. A collection of items of different types is also known as a heterogenous collection. In this case, we need to hide some of the typing information to declare such a collection.
文档可以包含文本、图片或表格。当我们处理文档时,我们希望将其所有组成部分放在一起,因此我们将它们存储在某个集合中。但是该集合的元素类型是什么?有几种方法可以实现这一点,所有这些方法都涉及隐藏一些类型信息。
A document can contain text, pictures, or tables. When we work with the document, we want to keep all its constituent parts together, so we will store them in some collection. But what is the type of the elements of that collection? There are several ways to implement this, all of which involve hiding some type information.
我们可以创建一个类层次结构,并说文档中的所有项目都必须是某个层次结构的一部分。如果一切都是 a DocumentItem,我们就可以存储值的集合DocumentItem,即使当我们向集合中添加项时,我们添加了Paragraph、Picture和 等类型Table。类似地,我们可以声明一个IDocumentItem接口并说该数组只包含实现该接口的类型,如以下清单所示。
We can create a class hierarchy and say that all items in the documents must be part of some hierarchy. If everything is a DocumentItem, we can store a collection of DocumentItem values even if, when we add items to the collection, we add types such as Paragraph, Picture, and Table. Similarly, we can declare an IDocumentItem interface and say that the array contains only types that implement this interface, as shown in the following listing.
接口 IDocumentItem { 1
/* ... */
}
类段落实现 IDocumentItem { 2
/* ... */
}
类图片实现 IDocumentItem { 2
/* ... */
}
类表实现 IDocumentItem { 2
/* ... */
}
类我的文档{
项目:IDocumentItem[]; 3个
/* ... */
}interface IDocumentItem { 1
/* ... */
}
class Paragraph implements IDocumentItem { 2
/* ... */
}
class Picture implements IDocumentItem { 2
/* ... */
}
class Table implements IDocumentItem { 2
/* ... */
}
class MyDocument {
items: IDocumentItem[]; 3
/* ... */
}
我们隐藏了一些类型信息,因此我们不再知道集合中的特定项目是 a Paragraph、 aPicture还是 a Table,但我们知道它实现了DocumentItemorIDocumentItem协定。如果我们只需要该契约指定的行为,我们可以按原样使用集合的元素。如果我们需要一个确切的类型,例如我们想要传递给图像增强插件的图片,我们必须将 or 向下DocumentItem转换IDocumentItem为Picture.
We’ve hidden some of the typing information, so we no longer know whether a particular item in the collection is a Paragraph, a Picture, or a Table, but we know that it implements the DocumentItem or IDocumentItem contract. If we need only behavior specified by that contract, we can work with the elements of the collection as is. If we need an exact type, such as a picture that we want to pass to an image-enhancing add-on, we have to downcast the DocumentItem or IDocumentItem back to a Picture.
如果我们预先知道我们正在处理的所有类型,我们可以使用求和类型,如清单 4.16所示。我们可以将我们的文档定义为一个数组Paragraph | Picture | Table(在这种情况下,我们必须通过其他方式跟踪集合中的每个项目是什么)或定义为一个类型Variant<Paragraph, Picture, Table>(它在内部跟踪它存储的类型)。
If we know up front all the types we are dealing with, we can use a sum type, as shown in listing 4.16. We can define our document as an array of Paragraph | Picture | Table (in which case we must track what each item in the collection is by some other means) or as a type such as Variant<Paragraph, Picture, Table> (which keeps track internally of the type it stores).
类段落 { 1
/* ... */
}
类图片 { 1
/* ... */
}
类表 { 1
/* ... */
}
类我的文档{
项目:(段落|图片|表格)[]; 2个
/* ... */
}class Paragraph { 1
/* ... */
}
class Picture { 1
/* ... */
}
class Table { 1
/* ... */
}
class MyDocument {
items: (Paragraph | Picture | Table)[]; 2
/* ... */
}
和选项Paragraph | Picture | Table都Variant<Paragraph, Picture, Table>允许我们存储一组不需要有任何共同点(没有共同的基类型或实现的接口)的项目。优点是我们不会对集合中的类型强加任何东西。缺点是如果不将列表中的项目转换回它们的实际类型,或者在这种情况下,Variant调用visit()并必须为集合中的每个可能类型提供函数,我们对列表中的项目无能为力。
Both Paragraph | Picture | Table and Variant<Paragraph, Picture, Table> options allow us to store a set of items that don’t need to have anything in common (no common base type or implemented interface). The advantage is that we don’t impose anything on the types in the collection. The disadvantage is that there is not much we can do with the items in the list without casting them back down to their actual types or, in the Variant case, calling visit()and having to provide functions for each of the possible types in the collection.
提醒一下,因为像这样的类型Variant在内部跟踪它实际存储的类型,就像它一样Either,它知道从传递给的一组函数中选择哪个函数visit()。
As a reminder, because a type like Variant keeps track internally of which type it actually stores, just as Either does, it knows which function to pick from a set of functions passed to visit().
在极端情况下,我们可以说我们有一个可以包含任何东西的集合。如清单 4.17所示,TypeScript 提供了unknown表示该类型集合的类型。大多数面向对象的编程语言都有一个共同的基类型,它是所有其他类型的父类,通常称为Object. 我们将在第 7 章讨论子类型时 深入讨论这个话题。
At an extreme, we can say we have a collection that can contain anything. As shown in listing 4.17, TypeScript provides the type unknown to represent that type of collection. Most object-oriented programming languages have a common base type that is the parent of all other types, usually called Object. We’ll cover this topic in depth in chapter 7 when we discuss subtyping.
类我的文档{
项目:未知[]; 1个
/* ... */
}class MyDocument {
items: unknown[]; 1
/* ... */
}
这种技术使我们能够拥有包含任何内容的文档。类型不需要共享契约,我们甚至不需要事先知道类型的作用。另一方面,我们对这个系列的元素能做的就更少了。我们几乎总是必须将它们转换为其他类型,因此我们必须以另一种方式跟踪它们的原始类型。
This technique allows us to have a document containing anything. Types don’t need to have a shared contract, and we don’t even need to know beforehand what the types do. On the other hand, there’s even less we can do with the elements of this collection. We’ll almost always have to cast them to other types, so we have to keep track of their original types in another way.
表 4.1总结了不同的方法和权衡。
Table 4.1 summarizes the different approaches and trade-offs.
|
优点 Pros |
缺点 Cons |
|
|---|---|---|
| 等级制度 | 无需转换即可轻松使用基类型的任何属性或方法 | 集合中的类型必须通过基类型或实现的接口相关联 |
| 和式 | 不要求类型相关 | 如果我们没有 Variant 的 visit(),则需要转换回实际类型以使用项目 |
| 未知类型 | 可以存储任何东西 | 需要跟踪实际类型并转换回它们以使用项目 |
所有这些例子都有利有弊,这取决于我们希望我们的集合在可以存储什么方面有多灵活,以及我们希望多久将项目恢复到它们的原始类型。也就是说,当我们将项目放入集合中时,所有示例都隐藏了一些类型信息。隐藏和恢复类型信息的另一个例子是序列化。
All these examples have pros and cons, depending on how flexible we want our collection to be in terms of what can be stored there and how often we expect to have to restore the items to their original types. That being said, all the examples hide some amount of type information when we put items in the collection. Another example of hiding and restoring type information is serialization.
当我们将信息写入文件并希望将其加载回来并在我们的程序中使用它时,或者当我们连接到互联网服务并发送和检索一些数据时,该数据以位序列的形式传输。序列化是获取特定类型的值并将其编码为位序列的过程。相反的操作,反序列化,涉及获取一个位序列并将其解码为我们可以使用的数据结构(图 4.7)。
When we write information to a file and want to load it back and use it in our program, or when we connect to an internet service and send and retrieve some data, that data travels as a sequence of bits. Serialization is the process of taking a value of a certain type and encoding it as a sequence of bits. The opposite operation, deserialization, involves taking a sequence of bits and decoding it into a data structure we can work with (figure 4.7).
确切的编码取决于我们使用的协议。它可以是 JSON、XML 或任何其他可用协议。从类型的角度来看,重要的部分是在序列化之后,我们最终得到的值应该等同于我们开始使用的类型化值,但是类型系统无法获得所有类型化信息。实际上,我们最终得到一个字符串或一个字节数组。该JSON.stringify()方法接受一个对象并返回一个 JSON 表示形式该对象作为字符串。如果我们将 a 字符串化Cat,如下一个清单所示,我们可以将结果写入磁盘、网络甚至屏幕,但我们无法将其写入meow()。
The exact encoding depends on the protocol we use. It can be JSON, XML, or any other of the multitude of available protocols. From a type perspective, the important part is that after serialization, we end up with a value that should be equivalent to the typed value we started with, but all typing information becomes unavailable to the type system. Effectively, we end up with a string or an array of bytes. The JSON.stringify() method takes an object and returns a JSON representation of that object as a string. If we stringify a Cat, as the next listing shows, we can write the result to disk, to the network, or even to the screen, but we cannot get it to meow().
类猫{
喵(){ 1
/* ... */
}
}
让 serializedCat: string = JSON.stringify(new Cat()); 2个
// serializeCat.meow(); 3个class Cat {
meow() { 1
/* ... */
}
}
let serializedCat: string = JSON.stringify(new Cat()); 2
// serializeCat.meow(); 3
我们仍然知道值是什么,但类型检查器不再知道了。相反的操作涉及获取一个序列化对象并将其转回类型化值。在这种情况下,我们可以使用该JSON.parse()方法,它接受一个字符串并返回一个 JavaScript 对象。因为这种技术适用于任何字符串,所以调用它的结果是 type any。
We still know what the value is, but the type checker no longer does. The opposite operation involves taking a serialized object and turning it back into a typed value. In this case, we can use the JSON.parse() method, which takes a string and returns a JavaScript object. Because this technique works for any string, the result of calling it is of type any.
TypeScript 提供了一种any类型。当键入信息不可用时,此类型用于与 JavaScript 的互操作性。any是一种危险的类型,因为编译器不对此类型的实例进行类型检查,它可以自由地与任何其他类型相互转换。开发人员应确保不会发生误解。
TypeScript provides an any type. This type is used for interoperability with JavaScript when typing information is unavailable. any is a dangerous type because the compiler does no type checking on instances of this type, which can be freely converted to and from any other type. It’s up to the developer to ensure that no misinterpretations happen.
如果我们知道我们有一个 serialized Cat,我们可以将它分配给一个新Cat对象Object.assign(),如下面的清单所示,然后将它转换回它的类型,因为Object.assign()返回一个 type 的值any。
If we know that we have a serialized Cat, we can assign it to a new Cat object by using Object.assign() as shown in the following listing, and then cast it back to its type, as Object.assign() returns a value of type any.
类猫{
喵() {
/* ... */
}
}
让 serializedCat: string = JSON.stringify(new Cat());
让反序列化猫:猫=
<Cat>Object.assign(new Cat(), JSON.parse(serializedCat)); 1个
deserializedCat.meow(); 2个class Cat {
meow() {
/* ... */
}
}
let serializedCat: string = JSON.stringify(new Cat());
let deserializedCat: Cat =
<Cat>Object.assign(new Cat(), JSON.parse(serializedCat)); 1
deserializedCat.meow(); 2
在某些情况下,我们可以获取并反序列化任意数量的可能类型,在这种情况下,在序列化对象中也对一些类型信息进行编码可能是个好主意。我们可以定义一个协议,其中每个对象都以代表其类型的字符为前缀。然后我们可以对 a 进行编码Cat并在结果字符串前加上"c"for Cat。如果我们得到一个序列化对象,我们检查第一个字符。如果是"c",我们可以安全地恢复我们的Cat. 如果是"d", 对于Dog,我们知道不要反序列化 a Cat,如以下清单所示。
In some cases, we can get and deserialize any number of possible types, in which case it might be a good idea to encode some of the typing information in the serialized object too. We can define a protocol in which each object is prefixed with a character that represents its type. Then we can encode a Cat and prefix the resulting string with "c" for Cat. If we get a serialized object, we check the first character. If it’s "c", we can safely restore our Cat. If it’s "d", for Dog, we know not to deserialize a Cat, as shown in the following listing.
类猫{
喵() { /* ... */ }
}
类狗{
树皮() { /* ... */ }
}
函数序列化猫(猫:猫):字符串{
返回 "c" + JSON.stringify(cat); 1个
}
函数序列化狗(狗:狗):字符串{
返回 "d" + JSON.stringify(dog); 2个
}
函数 tryDeserializeCat(from: string): Cat | 未定义 { 3
if (from[0] != "c") 返回未定义; 4个
返回 <Cat>Object.assign(new Cat(), JSON.parse(from.substr(1))); 5
}class Cat {
meow() { /* ... */ }
}
class Dog {
bark() { /* ... */ }
}
function serializeCat(cat: Cat): string {
return "c" + JSON.stringify(cat); 1
}
function serializeDog(dog: Dog): string {
return "d" + JSON.stringify(dog); 2
}
function tryDeserializeCat(from: string): Cat | undefined { 3
if (from[0] != "c") return undefined; 4
return <Cat>Object.assign(new Cat(), JSON.parse(from.substr(1))); 5
}
如果我们序列化一个Cat对象并调用tryDeserializeCat()它的序列化表示,我们会得到一个Cat对象。另一方面,如果我们序列化一个Dog对象并调用tryDeserializeCat(),我们会返回未定义的。然后我们可以检查我们是否得到了 anundefined并查看我们是否有Cat,如下一个清单所示。
If we serialize a Cat object and call tryDeserializeCat() on its serialized representation, we get back a Cat object. If, on the other hand, we serialize a Dog object and call tryDeserializeCat(), we get back undefined. Then we can check to see whether we got an undefined and see whether we have a Cat, as shown in the next listing.
让 catString: string = serializeCat(new Cat()); 1
让 dogString: string = serializeDog(new Dog()); 1个
让 maybeCat: 猫 | undefined = tryDeserializeCat(catString); 2个
if (maybeCat != undefined) { 3
let cat: Cat = <Cat>maybeCat; 4
猫.喵(); 4个
}
maybeCat = tryDeserializeCat(dogString); 5个let catString: string = serializeCat(new Cat()); 1
let dogString: string = serializeDog(new Dog()); 1
let maybeCat: Cat | undefined = tryDeserializeCat(catString); 2
if (maybeCat != undefined) { 3
let cat: Cat = <Cat>maybeCat; 4
cat.meow(); 4
}
maybeCat = tryDeserializeCat(dogString); 5
我们之所以可以maybeCat与进行比较,即使我们之前undefined无法Triangle与进行比较,是因为它是 TypeScript 中的一种特殊单位类型。该类型有一个可能的值,即. 在没有这种类型的情况下,我们总是可以使用像. 我们在第 3 章中将类型描述为包含类型值或无值的类型。 TLeftundefinedundefinedundefinedOptional<Cat>Optional<T>T
The reason why we can compare maybeCat with undefined, even though we couldn’t compare Triangle with TLeft previously, is that undefined is a special unit type in TypeScript. The undefined type has a single possible value, which is undefined. In the absence of this type, we can always use a type like Optional<Cat>. We described Optional<T> in chapter 3 as a type that contains a value of type T or nothing.
正如我们在本章中所看到的,类型为我们的代码提供了全新的安全级别。我们可以捕获类型声明中隐含的假设,并通过避免原始的痴迷并让类型检查器确保我们不会误解值来使它们显式。我们可以进一步限制某个类型的允许值,并确保在创建实例时满足约束,这样我们就有了一个保证,当我们有一个给定类型的实例时,它永远有效。
As we’ve seen throughout this chapter, types enable whole new levels of safety for our code. We can capture what would’ve been implicit assumptions in type declaration and make them explicit by avoiding primitive obsession and letting the type checker make sure that we don’t misinterpret values. We can further restrict the allowed values of a certain type and ensure that constraints are met during instance creation, so that we have a guarantee that when we have an instance of a given type, it will always be valid.
另一方面,我们希望在某些情况下更加灵活,以相同的方式处理多种类型。在这种情况下,我们可以隐藏一些类型信息并扩展变量可以取的可能值。在大多数情况下,我们仍然希望跟踪值的原始类型,以便稍后恢复它。我们通过将类型存储在其他地方(例如另一个变量)来在类型系统之外执行此操作。一旦我们不再需要额外的灵活性并希望再次依赖类型检查器,我们就可以通过使用类型转换来恢复类型。
On the other hand, we want to be more flexible in some situations and handle multiple types in the same way. In such situations, we can hide some of the type information and expand the possible values that a variable can take. In most cases, we would still like to keep track of the original type of the value so we can restore it later. We do that outside the type system by storing the type somewhere else, such as in another variable. As soon as we no longer need the extra flexibility and want to rely on the type checker again, we can restore the type by using a type cast.
如果我们想为它分配任何可能的值,我们应该使用哪种类型?
- any
- unknown
- any | unknown
- 要么any_unknown
Which type should we use if we want to assign any possible value to it?
- any
- unknown
- any | unknown
- Either any or unknown
- (number | string)[]
- number[] | string[]
- unknown[]
- any[]
What is the best way to represent an array of numbers and strings?
- (number | string)[]
- number[] | string[]
- unknown[]
- any[]
到目前为止,我们已经了解了基本类型、组合它们的方法以及我们可以利用类型系统来提高代码安全性的其他方法。在第 5 章中,我们将看到一些截然不同的东西:当我们可以将类型分配给函数并像对待代码中的任何其他值一样对待函数时,将会有哪些新的可能性向我们敞开?
So far we’ve looked at basic types, ways to compose them, and other ways in which we can leverage the type systems to increase the safety of our code. In chapter 5, we’ll look at something radically different: What new possibilities will be open to us when we can assign types to functions and treat functions like any other values in our code?
c—指定测量单位是一种更安全的方法。
c—Specifying the measurement unit is a safer approach.
这是一个可能的解决方案:
声明 const percentageType:唯一符号; 类百分比{ 只读值:数字; [百分比类型]:无效; 私有构造函数(值:数字){ this.value = 值; } 静态 makePercentage(值:数字):百分比 { 如果(值 < 0)值 = 0; 如果(值 > 100)值 = 100; 返回新的百分比(值); } }
Here is a possible solution:
declare const percentageType: unique symbol; class Percentage { readonly value: number; [percentageType]: void; private constructor(value: number) { this.value = value; } static makePercentage(value: number): Percentage { if (value < 0) value = 0; if (value > 100) value = 100; return new Percentage(value); } }
a—向上转型是安全的(将子类型转换为父类型)。
a—Upcasts are safe (casting child to parent type).
b—缩小转换是不安全的(可能会丢失信息)。
b—Narrowing casts are unsafe (might lose information).
b—unknown是比 更安全的选择any。
b—unknown is a safer option than any.
a—unknown并any删除过多的类型信息。
a—unknown and any remove too much type information.
本章涵盖
This chapter covers
我们介绍了基本类型和从它们构建的类型。我们还研究了如何声明新类型以提高程序的安全性并对它们的值实施各种约束。这是关于代数数据类型或将类型组合为求和类型和乘积类型的能力的极限。
We covered basic types and types built up from them. We also looked at how we can declare new types to increase the safety of our programs and enforce various constraints on their values. This is about as far as we can get with algebraic data types or the ability to combine types as sum types and product types.
我们将要介绍的类型系统的下一个特性,它打开了一个全新的表达世界,是类型函数的能力。如果我们可以命名函数类型并在我们使用其他类型的值的相同位置使用函数——如变量、参数和函数返回——我们可以简化几种常用结构的实现,并将常用算法抽象为库函数。
The next feature of type systems we are going to cover, which unlocks a whole new world of expressiveness, is the ability to type functions. If we can name function types and use functions in the same places we use values of other types—as variables, arguments, and function returns—we can simplify the implementation of several common constructs and abstract common algorithms to library functions.
在本章中,我们将研究如何简化策略设计模式的实施。(我们还将快速回顾一下该模式,以防您忘记它。)然后我们将讨论状态机以及如何使用函数属性更简洁地实现它们。我们将介绍惰性值,或者我们如何推迟昂贵的计算以希望我们不需要它。最后,我们将深入探讨基本的map()、reduce()和filter()算法。
In this chapter, we’ll look at how we can simplify the implementation of the strategy design pattern. (We’ll also have a quick refresher on the pattern, in case you forgot it.) Then we’ll talk about state machines and how they can be implemented more succinctly with function properties. We’ll cover lazy values, or how we can defer expensive computation in the hope that we won’t need it. Finally, we’ll deep dive into the fundamental map(), reduce(), and filter() algorithms.
所有这些应用程序都由功能类型启用,这是继基本类型及其组合之后类型系统发展的下一步。因为现在大多数编程语言都支持这些类型,所以我们将重新审视一些古老的、经过实践检验的概念。
All these applications are enabled by function types, the next step in the evolution of type systems after basic types and their combinations. Because most programming languages nowadays support these types, we’ll get a fresh look at some old, tried, and tested concepts.
最常用的设计模式之一是策略模式。策略设计模式是一种行为软件设计模式,可以在运行时从一系列算法中选择一种算法。它将算法与使用它们的组件解耦,从而提高了整个系统的灵活性。该模式通常如图 5.1所示。
One of the most commonly used design patterns is the strategy pattern. The strategy design pattern is a behavioral software design pattern that enables selecting an algorithm at run time from a family of algorithms. It decouples the algorithms from the components using them, which improves the flexibility of the overall system. The pattern is usually presented as in figure 5.1.
让我们看一个具体的例子。假设我们有一家洗车店,提供两种服务:标准洗车和高级洗车(额外支付 3 美元,提供额外抛光服务)。
Let’s look at a concrete example. Suppose that we have a car wash with two types of services: a Standard wash and a Premium wash (which, for an extra $3, provides additional polish).
我们可以将这个例子实现为一个策略,其中我们的IWashingStrategy接口提供了一个wash()方法。然后我们提供这个的两个实现界面:一个StandardWash和一个PremiumWash。Our是根据客户支付的服务将 an 应用于汽车的 CarWash上下文。IWashingStrategy.wash()
We can implement this example as a strategy, in which our IWashingStrategy interface provides a wash() method. Then we provide two implementations of this interface: a StandardWash and a PremiumWash. Our CarWash is the context that applies an IWashingStrategy.wash() to a car depending on which service the customer paid for.
类车{
/* 代表一辆车 */ 1
}
接口 IWashingStrategy { 2
洗(汽车:汽车):无效;
}
StandardWash 类实现 IWashingStrategy { 3
洗(车:车):无效{
/* 执行标准清洗 */
}
}
PremiumWash 类实现 IWashingStrategy { 3
洗(车:车):无效{
/* 执行高级清洗 */
}
}
类洗车 {
服务(汽车:汽车,保费:布尔值){
让洗涤策略:IWashingStrategy;
如果(高级){ 4
washingStrategy = new PremiumWash();
} 别的 {
washingStrategy = new StandardWash();
}
washingStrategy.wash(汽车); 4个
}
}class Car {
/* Represents a car */ 1
}
interface IWashingStrategy { 2
wash(car: Car): void;
}
class StandardWash implements IWashingStrategy { 3
wash(car: Car): void {
/* Perform standard wash */
}
}
class PremiumWash implements IWashingStrategy { 3
wash(car: Car): void {
/* Perform premium wash */
}
}
class CarWash {
service(car: Car, premium: boolean) {
let washingStrategy: IWashingStrategy;
if (premium) { 4
washingStrategy = new PremiumWash();
} else {
washingStrategy = new StandardWash();
}
washingStrategy.wash(car); 4
}
}
这段代码有效,但不必要地冗长。我们引入了一个接口和两个实现类型,每个类型都提供一个wash()方法。这些类型并不重要;我们代码中有价值的部分是清洗逻辑。这段代码只是一个函数,所以如果我们从接口和类转移到一个函数类型和两个具体实现,我们可以大大简化我们的代码。
This code works, but it is needlessly verbose. We’ve introduced an interface and two implementing types, each providing a single wash() method. These types are not really important; the valuable part of our code is the washing logic. This code is just a function, so we can simplify our code a lot if we move from interfaces and classes to a function type and the two concrete implementations.
我们可以定义WashingStrategy为一个类型,表示一个接收 aCar作为参数并返回 的函数void。然后我们可以将两种类型的清洗实现为两个函数——standardWash()和premimumWash()——都接受 aCar并返回void。他们CarWash可以选择其中之一应用于给定的汽车。
We can define WashingStrategy to be a type representing a function that receives a Car as an argument and returns void. Then we can implement the two types of washes as two functions—standardWash() and premimumWash()—both taking a Car and returning void. The CarWash can select one of them to apply to a given car.
类车{
/* 代表一辆车 */
}
类型 WashingStrategy = (car: Car) => void; 1个
函数 standardWash(汽车:汽车):void { 2
/* 执行标准清洗 */
}
函数 premiumWash(汽车:汽车):void { 2
/* 执行高级清洗 */
}
类洗车 {
服务(汽车:汽车,保费:布尔值){
让洗涤策略:洗涤策略; 3
3
如果(高级){ 3
washingStrategy = premiumWash;
} 别的 {
洗涤策略=标准洗涤;
}
洗涤策略(汽车); 4个
}
}class Car {
/* Represents a car */
}
type WashingStrategy = (car: Car) => void; 1
function standardWash(car: Car): void { 2
/* Perform standard wash */
}
function premiumWash(car: Car): void { 2
/* Perform premium wash */
}
class CarWash {
service(car: Car, premium: boolean) {
let washingStrategy: WashingStrategy; 3
3
if (premium) { 3
washingStrategy = premiumWash;
} else {
washingStrategy = standardWash;
}
washingStrategy(car); 4
}
}
如图 5.2所示,此实现比前一个实现的部分更少。
This implementation has fewer parts than the preceding one, as we can see in figure 5.2.
Let’s zoom in on the function type declaration, because we’re using one for the first time.
该函数standardWash()接受一个类型的参数Car并返回void,因此它的类型是从 Car 到 void 的函数,或者在 TypeScript 语法中,(car: Car) => void。function premiumWash(),即使它有不同的实现,也有完全相同的参数类型和返回类型,所以它有相同的类型。
The function standardWash() takes an argument of type Car and returns void, so its type is function from Car to void or, in TypeScript syntax, (car: Car) => void. The function premiumWash(), even though it has a different implementation, has exactly the same argument type and return type, so it has the same type.
函数的类型由其参数类型和返回类型给出。如果两个函数采用相同的参数并返回相同的类型,则它们具有相同的类型。参数集加上返回类型也称为函数的 签名。
The type of a function is given by the type of its arguments and its return type. If two functions take the same arguments and return the same type, they have the same type. The set of arguments plus return type is also known as the signature of a function.
我们想引用这个类型,所以我们通过声明给它一个名字type WashingStrategy = (car: Car) => void。每当我们使用WashingStrategyas 类型时,我们指的是函数类型(car: Car) => void。我们在方法中引用它CarWash.service()。
We want to refer to this type, so we give it a name by declaring type WashingStrategy = (car: Car) => void. Whenever we use WashingStrategy as a type, we mean the function type (car: Car) => void. We refer to it in the CarWash.service() method.
因为我们可以键入函数,所以我们可以有代表函数的变量。在我们的例子中,washingStrategy变量代表一个带有我们刚刚命名的签名的函数。我们可以分配任何接受 aCar并返回void给这个变量的函数。我们也可以像函数一样调用它。在第一个使用接口的示例中IWashingStrategy,我们通过调用washingStrategy.wash(car). 在我们的第二个例子中,其中washingStrategy是一个函数,我们简单地调用了washingStrategy(car).
Because we can type functions, we can have variables that represent functions. In our example, the washingStrategy variable represents a function with the signature we just named. We can assign any function that takes a Car and returns void to this variable. We can also call it as we would a function. In the first example that used an IWashingStrategy interface, we ran our car-washing logic by calling washingStrategy.wash(car). In our second example, in which washingStrategy is a function, we simply called washingStrategy(car).
将函数分配给变量并像类型系统中的任何其他值一样对待它们的能力导致了所谓的 一流函数。这意味着该语言将函数视为一等公民,赋予它们与其他值相同的权利:它们有类型;它们可以分配给变量并作为参数传递,检查有效性,并转换(如果兼容)为其他类型。
The ability to assign functions to variables and treat them like any other values in the type system results in what are called first-class functions. That means the language treats functions like first-class citizens, granting them the same rights as other values: they have types; and they can be assigned to variables and passed around as arguments, checked for validity, and converted (if compatible) to other types.
早些时候,我们看到了两种实现策略模式的方法。对比这两个实现,第一个例子中的书本策略实现需要很多额外的机制:我们需要声明一个接口,我们需要有多个实现该接口的类来提供策略的具体逻辑。第二种实现归结为我们要实现的本质:我们有两个实现逻辑的函数,我们直接引用它们。
Earlier, we saw two ways to implement a strategy pattern. Contrasting the two implementations, the by-the-book strategy implementation in the first example requires a lot of extra machinery: we need to declare an interface, and we need to have multiple classes implementing that interface to provide the concrete logic of the strategy. The second implementation is boiled down to the essence of what we are trying to achieve: we have two functions implementing the logic, and we refer to them directly.
两种实现实现相同的目标。依赖接口的第一种之所以更为普遍,是因为当设计模式在 20 世纪 90 年代风靡一时时,并不是所有的主流编程语言都支持一阶函数。事实上,他们中很少有人这样做。这已不再是这种情况。大多数语言现在都可以键入函数,我们可以利用这种能力来提供一些设计模式的更简洁的实现。
Both implementations achieve the same goals. The reason why the first one, which relies on interfaces, is more widespread is that when design patterns became all the rage in the 1990s, not all mainstream programming languages supported first-order functions. In fact, few of them did. This is no longer the case. Most languages can type functions now, and we can leverage that capability to provide more-succinct implementations of some design patterns.
重要的是要记住模式是相同的:我们仍在封装一系列算法并在运行时选择要使用的算法。不同之处在于实施,现代功能使我们能够更轻松地表达。我们将一个接口和两个类(每个类实现一个方法)替换为一个类型声明和两个函数。
It’s important to keep in mind that the pattern is the same: we are still encapsulating a family of algorithms and selecting at run time the one to use. The difference is in the implementation, which modern capabilities allow us to express more easily. We’re replacing an interface and two classes (each class implementing a method) with a type declaration and two functions.
在大多数情况下,更简洁的实现就足够了。当算法不能表示为简单函数时,我们可能需要重新考虑接口和类的实现。有时,我们需要多个功能或需要跟踪某些状态,在这种情况下,第一个实现会更适合,因为它将策略的相关部分分组在一个通用类型下。
In most cases, the more-succinct implementation is enough. We might need to reconsider the interface and classes implementation when the algorithms are not representable as simple functions. Sometimes, we need multiple functions or need to track some state, in which case the first implementation would be better suited, as it groups the related pieces of a strategy under a common type.
在我们继续之前,让我们快速回顾一下本节中介绍的一些术语:
Before we move on, let’s quickly review some of the terms introduced in this section:
函数添加(x:数字,y:数字):数字{
返回 x + y;
}
函数减法(x:数字,y:数字):数字{
返回 x - y;
}
function add(x: number, y: number): number {
return x + y;
}
function subtract(x: number, y: number): number {
return x - y;
}
isEven()接受数字作为参数并true在数字为偶数时返回的函数的类型是什么false?
- [number, boolean]
- (x: number) => boolean
- (x: number, isEven: boolean)
- {x: number, isEven: boolean}
What is the type of a function isEven() that takes a number as an argument and returns true if the number is even and false otherwise?
- [number, boolean]
- (x: number) => boolean
- (x: number, isEven: boolean)
- {x: number, isEven: boolean}
check()接受一个数字和一个相同类型的函数作为isEven()参数并返回将给定函数应用于给定值的结果的函数 的类型是什么?
- (x: number, func: number) => boolean
- (x: number) => (x: number) => boolean
- (x: number, func: (x: number) => boolean) => boolean
- (x: number, func: (x: number) => boolean) => void
What is the type of a function check() that takes a number and a function of the same type as isEven() as arguments and returns the result of applying the given function to the given value?
- (x: number, func: number) => boolean
- (x: number) => (x: number) => boolean
- (x: number, func: (x: number) => boolean) => boolean
- (x: number, func: (x: number) => boolean) => void
一等函数的一个非常有用的应用使我们能够将类的属性定义为具有函数类型。然后我们可以为它分配不同的功能,在运行时改变行为。这作为类的插件方法,我们可以根据需要交换它。
One very useful application of first-class functions enables us to define a property of a class as having a function type. Then we can assign different functions to it, changing the behavior at run time. This acts as a plug-in method on the class, and we can swap it as needed.
Greeter例如,我们可以实现一个可插入的。我们不是实现greet()方法,而是实现greet具有函数类型的属性。然后我们可以给它分配多个问候函数,比如sayGoodMorning()和sayGoodNight()。
We can implement a pluggable Greeter, for example. Instead of implementing a greet() method, we implement a greet property with a function type. Then we can assign multiple greeting functions to it, such as sayGoodMorning() and sayGoodNight().
函数 sayGoodMorning(): void { 1
console.log("早上好!");
}
函数 sayGoodNight(): void { 1
console.log("晚安!");
}
类迎宾{
问候语:() => void = sayGoodMorning; 2个
}
let greeter: Greeter = new Greeter();
问候语。问候语(); 3个
greeter.greet = sayGoodNight; 4个
问候语。问候语(); 5个function sayGoodMorning(): void { 1
console.log("Good morning!");
}
function sayGoodNight(): void { 1
console.log("Good night!");
}
class Greeter {
greet: () => void = sayGoodMorning; 2
}
let greeter: Greeter = new Greeter();
greeter.greet(); 3
greeter.greet = sayGoodNight; 4
greeter.greet(); 5
这遵循上一节中讨论的策略模式实现,但值得注意的是,这种方法使我们能够轻松地将可插入行为添加到类中。如果我们想添加一个新的问候语,我们只需要添加另一个具有相同签名的函数并将其分配给该greet属性。
This follows from the strategy pattern implementation discussed in the previous section, but it’s worth noting that this approach enables us to easily add pluggable behavior to a class. If we want to add a new greeting, we simply need to add another function with the same signature and assign it to the greet property.
在撰写本书的初稿时,我编写了一个小脚本来帮助我保持源代码与文本的同步。草稿是用流行的 Markdown 格式编写的。我将源代码保存在单独的 TypeScript 文件中,这样我就可以编译它们并确保即使我更新了代码示例,它们仍然可以工作。
While working on an early draft of this book, I wrote a small script to help me keep the source code in sync with the text. The draft was written in the popular Markdown format. I kept the source code in separate TypeScript files so I could compile them and ensure that even if I update the code samples, they’ll still work.
我需要一种方法来确保 Markdown 文本始终包含最新的代码示例。代码示例始终出现在包含 的行```ts和包含 的行之间```。当 HTML 从 Markdown 源生成时,```ts被解释为 TypeScript 代码块的开始,它使用 TypeScript 语法高亮显示,同时 ```标记该代码块的结尾。这些代码块的内容必须从我可以在文本之外编译和验证的实际 TypeScript 源文件中内联(图 5.3)。
I needed a way to ensure that the Markdown text always contains the latest code samples. The code samples always appear between a line containing ```ts and a line containing ```. When HTML is generated from the Markdown source, ```ts is interpreted as the beginning of a TypeScript code block, which gets rendered with TypeScript syntax highlighting, whereas ``` marks the end of that code block. The contents of these code blocks had to be inlined from actual TypeScript source files that I could compile and validate outside the text (figure 5.3).
为了确定哪个代码示例去了哪里,我依靠了一个小技巧。Markdown 允许在文档文本中使用原始 HTML,所以我用HTML 注释,例如<!-- sample1 -->. HTML 注释不会被渲染,所以当 Markdown 转换为 HTML 时,它们变得不可见。另一方面,我的脚本可以使用这些注释来确定在何处内联哪个代码示例。
To determine which code sample went where, I relied on a small trick. Markdown allows raw HTML in the document text, so I annotated each code sample with an HTML comment, such as <!-- sample1 -->. HTML comments do not get rendered, so when Markdown is converted to HTML, they became invisible. On the other hand, my script could use these comments to determine which code sample to inline where.
当所有代码示例都从磁盘加载后,我必须处理草稿的每个 Markdown 文档并生成更新版本,如下所示:
When all code samples were loaded from disk, I had to process each Markdown document of the draft and produce an updated version as follows:
每次运行时,文档中前面带有<!-- ... -->标记的现有代码示例都会更新为磁盘上最新版本的 TypeScript 文件。前面没有的其他代码块<!-- ... -->不会更新,因为它们是在文本处理模式下处理的。
With each run, the existing code samples in the document preceded by a <!-- ... --> marker get updated to the latest version of the TypeScript files on disk. Other code blocks that aren’t preceded by <!-- ... --> don’t get updated, as they are processed in text processing mode.
例如,这里有一个 helloWorld.ts 代码示例。
As an example, here is a helloWorld.ts code sample.
console.log("你好世界!");console.log("Hello world!");
我们想将此代码嵌入到 Chapter1.md 中并确保它保持最新,如下一个清单所示。
We want to embed this code in Chapter1.md and make sure that it’s kept up to date, as shown in the next listing.
# 第1章
打印“Hello world!”。
<!-- 你好世界 -->
```ts
console.log("你好"); 1```
_# Chapter 1
Printing "Hello world!".
<!-- helloWorld -->
```ts
console.log("Hello"); 1
```
该文档逐行处理如下:
This document gets processed line by line as follows:
我们的文本处理脚本的行为最好建模为状态机。状态机具有一组状态和一组状态对之间的转换。机器以给定状态启动,也称为启动状态;如果满足某些条件,它可以转换到另一个状态。
The behavior of our text processing script is best modeled as a state machine. A state machine has a set of states and a set of transitions between pairs of states. The machine starts in a given state, also known as the start state; if certain conditions are met, it can transition to another state.
这正是我们的文本处理器使用其三种处理模式所做的。输入行在 文本处理模式下以一定的方式处理。当满足某些条件(<!-- sample -->遇到标记)时,我们的处理器将转换为标记处理模式。同样,当满足某些其他条件(```ts遇到代码块标记)时,它会转换为代码处理模式。当遇到代码块标记的结尾 ( ```) 时,它会转换回文本处理模式(图 5.4)。
This is exactly what our text processor does with its three processing modes. Input lines are processed in a certain way in text processing mode. When some condition is met (a <!-- sample --> marker is encountered), our processor transitions to marker processing mode. Again, when some other condition is met (a ```ts code-block marker is encountered), it transitions to code processing mode. When the end of the code-block marker is encountered (```), it transitions back to text processing mode (figure 5.4).
现在我们已经对解决方案进行了建模,让我们看看我们将如何实施它。实现状态机的一种方法是将状态集定义为枚举,跟踪当前状态,并使用涵盖switch所有可能状态的语句获得所需的行为。在我们的例子中,我们可以定义一个TextProcessingMode枚举。
Now that we’ve modeled the solution, let’s look at how we would implement it. One way to implement a state machine is to define the set of states as an enumeration, keeping track of the current state, and get the desired behavior with a switch statement that covers all possible states. In our case, we can define a TextProcessingMode enum.
我们的TextProcessor类将跟踪属性中的当前状态并在方法中mode实现语句。根据状态,此方法将依次调用三种处理方法之一:、或。这些函数将实现文本处理,然后在适当的时候通过更新当前状态转换到另一个状态。 switchprocess-Line()processTextLine()processMarkerLine()process-CodeLine()
Our TextProcessor class will keep track of the current state in a mode property and implement the switch statement in a process-Line() method. Depending on the state, this method will in turn invoke one of the three processing methods: processTextLine(), processMarkerLine(), or process-CodeLine(). These functions will implement the text processing and then, when appropriate, transition to another state by updating the current state.
处理由多行文本组成的 Markdown 文档意味着使用我们的状态机依次处理每一行,然后将最终结果返回给调用者,如下一个清单所示。
Processing a Markdown document consisting of multiple lines of text means processing each line in turn, using our state machine, and then returning the final result to the caller, as shown in the next listing.
枚举 TextProcessingMode { 1
文本,
标记,
代码,
}
类 TextProcessor {
私有模式:TextProcessingMode = TextProcessingMode.Text;
私有结果:string[] = [];
私有代码示例:string[] = [];
processText(行:字符串[]):字符串[] {
这个.result = [];
this.mode = TextProcessingMode.Text;
for (let line of lines) { 2
this.processLine(线);
}
返回这个结果;
}
私人流程线(行:字符串):void {
开关(this.mode){ 3
案例 TextProcessingMode.Text:
this.processTextLine(行);
休息;
案例 TextProcessingMode.Marker:
this.processMarkerLine(线);
休息;
案例 TextProcessingMode.Code:
this.processCodeLine(行);
休息;
}
}
private processTextLine(line: string): void { 4
this.result.push(line);
如果 (line.startsWith("<!--")) { 4
this.loadCodeSample(行);
this.mode = TextProcessingMode.Marker;
}
}
私有 processMarkerLine(line: string): void { 5
this.result.push(line);
如果 (line.startsWith("```ts")) { 5
this.result = this.result.concat(this.codeSample);
this.mode = TextProcessingMode.Code;
}
}
private processCodeLine(line: string): void { 6
if (line.startsWith("```")) { 6
this.result.push(line);
this.mode = TextProcessingMode.Text;
}
}
私人 loadCodeSample(行:字符串){ 7
/* 根据marker加载sample,存入this.codeSample */
}
}enum TextProcessingMode { 1
Text,
Marker,
Code,
}
class TextProcessor {
private mode: TextProcessingMode = TextProcessingMode.Text;
private result: string[] = [];
private codeSample: string[] = [];
processText(lines: string[]): string[] {
this.result = [];
this.mode = TextProcessingMode.Text;
for (let line of lines) { 2
this.processLine(line);
}
return this.result;
}
private processLine(line: string): void {
switch (this.mode) { 3
case TextProcessingMode.Text:
this.processTextLine(line);
break;
case TextProcessingMode.Marker:
this.processMarkerLine(line);
break;
case TextProcessingMode.Code:
this.processCodeLine(line);
break;
}
}
private processTextLine(line: string): void { 4
this.result.push(line);
if (line.startsWith("<!--")) { 4
this.loadCodeSample(line);
this.mode = TextProcessingMode.Marker;
}
}
private processMarkerLine(line: string): void { 5
this.result.push(line);
if (line.startsWith("```ts")) { 5
this.result = this.result.concat(this.codeSample);
this.mode = TextProcessingMode.Code;
}
}
private processCodeLine(line: string): void { 6
if (line.startsWith("```")) { 6
this.result.push(line);
this.mode = TextProcessingMode.Text;
}
}
private loadCodeSample(line: string) { 7
/* Load sample based on marker, store in this.codeSample */
}
}
我们省略了从外部文件加载示例的代码,因为它与我们的状态机讨论并不特别相关。此实现有效,但如果我们使用可插入功能,则可以简化。
We omitted the code to load a sample from an external file, as it isn’t particularly relevant to our state machine discussion. This implementation works but can be simplified if we use a pluggable function.
请注意,我们所有的文本处理函数都具有相同的签名:它们将一行文本作为参数string并返回void。如果我们不是processLine()实现一个大switch语句并转发到适当的函数,而是创建processLine()这些函数之一呢?
Note that all our text processing functions have the same signature: they take a line of text as a string argument and return void. What if, instead of having processLine() implement a big switch statement and forward to the appropriate function, we make processLine() one of those functions?
processLine()我们可以将其定义为具有 type 的类的属性,(line: string) => void并使用 对其进行初始化,而不是作为方法实现process-TextLine(),如以下代码所示。mode然后,在三种文本处理方法中的每一种中,我们都设置为不同的processLine方法,而不是设置为不同的枚举值。事实上,我们不再需要从外部跟踪我们的状态。我们甚至不需要枚举!
Instead of implementing processLine() as a method, we can define it as a property of the class with type (line: string) => void and initialize it with process-TextLine(), as shown in the following code. Then, in each of the three text processing methods, instead of setting mode to a different enum value, we set processLine to a different method. In fact, we no longer need to keep track of our state externally. We don’t even need an enum!
类 TextProcessor {
私有结果:string[] = [];
private processLine: (line: string) => void = this.processTextLine;
私有代码示例:string[] = [];
processText(行:字符串[]):字符串[] {
这个.result = [];
this.processLine = this.processTextLine;
for (let line of lines) {
this.processLine(线);
}
返回这个结果;
}
私有 processTextLine(行:字符串):void {
this.result.push(line);
如果 (line.startsWith("<!--")) {
this.loadCodeSample(行);
this.processLine = this.processMarkerLine; 1个
}
}
私有 processMarkerLine(行:字符串):void {
this.result.push(line);
如果 (line.startsWith("```ts")) {
this.result = this.result.concat(this.codeSample);
this.processLine = this.processCodeLine; 1个
}
}
私有 processCodeLine(行:字符串):void {
如果(line.startsWith(“```”)){
this.result.push(line);
this.processLine = this.processTextLine; 1个
}
}
私有 loadCodeSample(行:字符串){
/* 根据marker加载sample,存入this.codeSample */
}
}class TextProcessor {
private result: string[] = [];
private processLine: (line: string) => void = this.processTextLine;
private codeSample: string[] = [];
processText(lines: string[]): string[] {
this.result = [];
this.processLine = this.processTextLine;
for (let line of lines) {
this.processLine(line);
}
return this.result;
}
private processTextLine(line: string): void {
this.result.push(line);
if (line.startsWith("<!--")) {
this.loadCodeSample(line);
this.processLine = this.processMarkerLine; 1
}
}
private processMarkerLine(line: string): void {
this.result.push(line);
if (line.startsWith("```ts")) {
this.result = this.result.concat(this.codeSample);
this.processLine = this.processCodeLine; 1
}
}
private processCodeLine(line: string): void {
if (line.startsWith("```")) {
this.result.push(line);
this.processLine = this.processTextLine; 1
}
}
private loadCodeSample(line: string) {
/* Load sample based on marker, store in this.codeSample */
}
}
第二种实现摆脱了TextProcessingMode枚举、mode属性和switch将处理转发到适当方法的语句。而不是处理转发,processLine现在是合适的处理方式。
The second implementation gets rid of the TextProcessingMode enum, the mode property, and the switch statement that forwarded processing to the appropriate method. Instead of handling forwarding, processLine now is the appropriate processing method.
此实现消除了单独跟踪状态并使其与处理逻辑保持同步的需要。如果我们想引入一个新的状态,旧的实现会迫使我们更新几个地方的代码。除了实现新的处理逻辑和状态转换之外,我们还必须更新枚举并向语句中添加另一个 case switch。我们的替代实现消除了对该任务的需求:状态完全由函数表示。
This implementation removes the need to keep track of states separately and keep that in sync with the processing logic. If we ever wanted to introduce a new state, the old implementation would’ve forced us to update the code in several places. Besides implementing the new processing logic and state transitions, we would’ve had to update the enum and add another case to the switch statement. Our alternative implementation removes the need for that task: a state is represented purely by a function.
需要注意的是,对于具有许多状态的状态机,显式捕获状态甚至转换可能会使代码更易于理解。尽管如此,switch另一种可能的实现不是使用枚举和语句,而是将每个状态表示为一个单独的类型,并将整个状态机表示为可能状态的总和类型,从而允许我们将其分解为类型安全的组件。以下是我们如何使用求和类型实现状态机的示例。代码有点冗长,所以如果可能的话,我们应该尝试我们目前讨论的实现,这是基于 的switch状态机的另一种替代方法。
One caveat is that for state machines with many states, capturing states and even transitions explicitly might make the code easier to understand. Even so, instead of using enums and switch statements, another possible implementation represents each state as a separate type and the whole state machine as a sum type of the possible states, allowing us to break it apart into type-safe components. Following is an example of how we would implement the state machine by using a sum type. The code is a bit more verbose, so if possible, we should try the implementation we’ve discussed so far, which is another alternative to a switch-based state machine.
当使用求和类型时,每个状态由不同的类型表示,因此我们有 a TextLineProcessor、 aMarkerLineProcessor和 a CodeLine-Processor。每个都跟踪成员中到目前为止已处理的行result,并提供一种process()方法来处理一行文本。
When a sum type is used, each state is represented by a different type, so we have a TextLineProcessor, a MarkerLineProcessor, and a CodeLine-Processor. Each keeps track of the processed lines so far in a result member and provides a process() method to handle a line of text.
求和型状态机
State machine with sum type
类 TextLineProcessor {
结果:字符串[];
构造函数(结果:字符串[]){
this.result = 结果;
}
过程(行:字符串):TextLineProcessor | 标记线处理器 { 1
this.result.push(line);
如果 (line.startsWith("<!--")) { 2
返回新的标记线处理器(
this.result, this.loadCodeSample(line));
} 别的 {
归还这个;
}
}
private loadCodeSample(line: string): string[] {
/* 根据marker加载sample,存入this.codeSample */
}
}
类 MarkerLineProcessor {
结果:字符串[];
代码示例:字符串[]
构造函数(结果:字符串[],代码示例:字符串[]){
this.result = 结果;
this.codeSample = codeSample;
}
过程(行:字符串):MarkerLineProcessor | 代码线处理器 { 3
this.result.push(line);
如果 (line.startsWith("```ts")) { 4
this.result = this.result.concat(this.codeSample);
返回新的 CodeLineProcessor(this.result);
} 别的 {
归还这个;
}
}
}
类 CodeLineProcessor {
结果:字符串[];
构造函数(结果:字符串[]){
this.result = 结果;
}
过程(行:字符串):CodeLineProcessor | TextLineProcessor { 5
if (line.startsWith("```")) { 6
this.result.push(line);
返回新的 TextLineProcessor(this.result);
} 别的 {
归还这个;
}
}
}
函数 processText(行:字符串):字符串 [] {
让处理器:TextLineProcessor | 标记线处理器 7
| CodeLineProcessor = new TextLineProcessor([]);
for (let line of lines) {
processor = processor.process(line); 8个
}
返回处理器.result;
}class TextLineProcessor {
result: string[];
constructor(result: string[]) {
this.result = result;
}
process(line: string): TextLineProcessor | MarkerLineProcessor { 1
this.result.push(line);
if (line.startsWith("<!--")) { 2
return new MarkerLineProcessor(
this.result, this.loadCodeSample(line));
} else {
return this;
}
}
private loadCodeSample(line: string): string[] {
/* Load sample based on marker, store in this.codeSample */
}
}
class MarkerLineProcessor {
result: string[];
codeSample: string[]
constructor(result: string[], codeSample: string[]) {
this.result = result;
this.codeSample = codeSample;
}
process(line: string): MarkerLineProcessor | CodeLineProcessor { 3
this.result.push(line);
if (line.startsWith("```ts")) { 4
this.result = this.result.concat(this.codeSample);
return new CodeLineProcessor(this.result);
} else {
return this;
}
}
}
class CodeLineProcessor {
result: string[];
constructor(result: string[]) {
this.result = result;
}
process(line: string): CodeLineProcessor | TextLineProcessor { 5
if (line.startsWith("```")) { 6
this.result.push(line);
return new TextLineProcessor(this.result);
} else {
return this;
}
}
}
function processText(lines: string): string[] {
let processor: TextLineProcessor | MarkerLineProcessor 7
| CodeLineProcessor = new TextLineProcessor([]);
for (let line of lines) {
processor = processor.process(line); 8
}
return processor.result;
}
我们所有的处理器都返回一个处理器实例:this,如果没有状态变化,或者一个新的处理器作为状态变化。通过调用每一行文本并通过将其重新分配给方法调用的结果来更新状态更改来 运行processText()状态机。process()processor
All our processors return a processor instance: this, if there is no state change, or a new processor as state changes. The processText() runs the state machine by calling process() on each line of text and updating processor as state changes by reassigning it to the result of the method call.
现在状态集在变量的签名中明确说明processor,可以是 a TextLineProcessor、 aMarkerLineProcessor或 a CodeLineProcessor。
Now the set of states is spelled out explicitly in the signature of the processor variable, which can be a TextLineProcessor, a MarkerLineProcessor, or a CodeLineProcessor.
可能的转换在方法的签名中被捕获process()。TextLineProcessor.process例如,返回TextLineProcessor | MarkerLine-Processor,因此它可以保持相同的状态 ( TextLineProcessor) 或转换到该MarkerLineProcessor状态。如果需要,这些状态类可以有更多的属性和成员。这个实现比依赖函数的实现稍微长一些,所以如果我们不需要额外的特性,我们最好使用更简单的解决方案。
The possible transitions are captured in the signatures of the process() methods. TextLineProcessor.process returns TextLineProcessor | MarkerLine-Processor, for example, so it can stay in the same state (TextLineProcessor) or transition to the MarkerLineProcessor state. These state classes can have more properties and members if needed. This implementation is slightly longer than the one that relies on functions, so if we don’t need the extra features, we are better off using the simpler solution.
让我们快速回顾一下本节中讨论的替代实现,然后看看函数类型的其他应用:
Let’s quickly review the alternative implementations discussed in this section and then look at other applications of function types:
我们对状态机的讨论到此结束。在下一节中,我们将了解函数类型的另一种用途:实现惰性求值。
This concludes our discussion of state machines. In the next section, we look at another use of function types: implementing lazy evaluation.
对可以是open或closed作为状态机的简单连接建模。连接打开connect和关闭disconnect。
Model a simple connection that can be open or closed as a state machine. A connection is opened with connect and closed with disconnect.
将前面的连接实现为具有功能的功能状态机process()。在关闭的连接中,process()打开一个连接。在打开的连接中,process()调用read()返回字符串的函数。如果字符串为空,则关闭连接;否则,读取的字符串将记录到控制台。read()给出为declare function read(): string;。
Implement the preceding connection as a functional state machine with a process() function. In a closed connection, process() opens a connection. In an open connection, process() calls a read() function that returns a string. If the string is empty, the connection is closed; otherwise, the read string is logged to the console. read() is given as declare function read(): string;.
能够将函数用作任何其他值的另一个优点是我们可以存储它们并仅在需要时调用它们。有时,我们可能想要的值的计算成本很高。假设我的程序可以构建 aBike和 a Car。我可能想要一个Car. 但是 a 的Car建造成本很高,所以我可能会决定骑自行车。A 的Bike建造成本非常低,所以我不担心成本。与其总是在Car每次运行程序时构建一个,只是为了我可以在需要时使用它,让我能够请求一个不是更好吗Car?在那种情况下,我会在真正需要它的时候请求它,然后执行昂贵的构建逻辑。如果我从不要求它,就不会浪费任何资源。
Another advantage of being able to use functions as any other value is that we can store them and invoke them only when needed. Sometimes, a value we may want is expensive to compute. Let’s say that my program can build a Bike and a Car. I may want a Car. But a Car is expensive to build, so I might decide to ride my bike instead. A Bike is extremely cheap to build, so I’m not worried about the cost. Instead of always building a Car with each run of the program, just so I can use it if I want it, wouldn’t it be better to give me the ability to ask for a Car? In that case, I would ask for it when I really needed it and execute the expensive building logic then. If I never asked for it, no resources would be wasted.
这个想法是尽可能推迟昂贵的计算,希望它可能根本不需要。因为计算被表示为函数,所以我们可以传递函数而不是实际值,并在我们需要这些值时调用它们。这个过程称为惰性求值。相反的是急切求值,我们立即产生值并传递它们,即使我们稍后决定丢弃它们也是如此。
The idea is to postpone expensive computation as much as possible, in the hope that it may not be needed at all. Because computation is expressed as functions, we can pass around functions instead of actual values and call them when and whether we need the values. This process is called lazy evaluation. The opposite is eager evaluation, in which we produce the values immediately and pass them around even if we decide later to discard them.
自行车类 { } 1
类汽车 { } 1
函数 chooseMyRide(bike: Bike, car: Car): Bike | 汽车 { 2
如果(isItRaining()){
还车;
} 别的 {
归还自行车;
}
}
选择我的骑行(新自行车(),新汽车()); 3个class Bike { } 1
class Car { } 1
function chooseMyRide(bike: Bike, car: Car): Bike | Car { 2
if (isItRaining()) {
return car;
} else {
return bike;
}
}
chooseMyRide(new Bike(), new Car()); 3
在我们急切的Car生产示例中,要调用chooseMyRide(),我们需要提供一个Car对象,所以我们已经支付了构建一个Car. 如果天气好并且我决定骑自行车,那么这个Car实例就是免费创建的。
In our eager Car production example, to call chooseMyRide(), we need to supply a Car object, so we’re already paying the cost of building a Car. If the weather is nice and I decide to ride my bike, the Car instance was created for nothing.
让我们切换到惰性方法。我们不提供 a Car,而是提供一个在调用时返回 a 的函数Car。
Let’s switch to a lazy approach. Instead of providing a Car, let’s provide a function that returns a Car when called.
类自行车{}
类车{}
function chooseMyRide(bike: Bike, car: () => Car ): 自行车 | 汽车 { 1
如果(isItRaining()){
返回汽车() ; 2个
} 别的 {
归还自行车;
}
}
函数 makeCar(): 汽车 { 3
返回新车();
}
chooseMyRide(新自行车(), makeCar ); 3个class Bike { }
class Car { }
function chooseMyRide(bike: Bike, car: () => Car): Bike | Car { 1
if (isItRaining()) {
return car(); 2
} else {
return bike;
}
}
function makeCar(): Car { 3
return new Car();
}
chooseMyRide(new Bike(), makeCar); 3
Car除非确实需要,否则惰性版本不会产生昂贵的开销。如果我决定改为骑自行车,则该函数永远不会被调用,也不会Car被创建。
The lazy version will not create an expensive Car unless it’s really needed. If I decide to ride my bike instead, the function never gets called, and no Car gets created.
这是我们可以通过纯面向对象的构造来实现的,尽管需要更多的代码。我们可以声明一个CarFactory包装方法的类makeCar(),并将其用作chooseMyRide(). CarFactory然后我们将在调用时创建一个新实例chooseMyRide(),该实例将调用需要时的方法。但是,当我们可以写得更少时,为什么还要写更多的代码呢?事实上,我们可以让我们的代码更短。
This is something we could achieve with pure object-oriented constructs, albeit with a lot more code. We could declare a CarFactory class that wraps a makeCar() method and use that as the argument to chooseMyRide(). We would then create a new instance of CarFactory when calling chooseMyRide(), which would invoke the method when needed. But why write more code when we can write less? In fact, we can make our code even shorter.
大多数现代编程语言都支持匿名函数或lambda。Lambda 类似于普通函数,但没有名称。每当我们有一个一次性函数时,我们都会使用 lambdas:一个我们通常只引用一次的函数,因此为它命名的麻烦变成了额外的工作。相反,我们可以提供内联实现。
Most modern programming languages support anonymous functions, or lambdas. Lambdas are similar to normal functions but don’t have names. We would use lambdas whenever we have a one-off function: a function we usually refer to only once, so going through the trouble of naming it becomes extra work. Instead, we can provide an inline implementation.
在我们的惰性汽车示例中,一个很好的候选者是makeCar()。因为chooseMyRide()需要一个不带参数且返回 a 的函数Car,所以我们必须声明这个我们只引用一次的新函数:当我们将它作为参数传递给chooseMyRide(). 我们可以使用匿名函数代替此函数,如以下清单所示。
In our lazy car example, a good candidate is makeCar(). Because chooseMyRide() needs a function with no arguments that returns a Car, we had to declare this new function that we refer to only once: when we pass it as an argument to chooseMyRide(). Instead of this function, we can use an anonymous function, as shown in the following listing.
类自行车{}
类车{}
函数 chooseMyRide(bike: Bike, car: () => Car): Bike | 车 {
如果(isItRaining()){
回车();
} 别的 {
归还自行车;
}
}
chooseMyRide(new Bike(), () => new Car() ); 1个class Bike { }
class Car { }
function chooseMyRide(bike: Bike, car: () => Car): Bike | Car {
if (isItRaining()) {
return car();
} else {
return bike;
}
}
chooseMyRide(new Bike(), () => new Car()); 1
TypeScript lambda 语法与函数类型声明非常相似:我们在括号中有参数列表(在这种特殊情况下没有参数),然后是=>函数体。如果函数有多行,我们会把它们放在{and之间},,但是因为我们只有一次调用new Car(),这被隐式地认为是 lambda 的返回语句,所以我们去掉makeCar()构造逻辑并将其放在单线。
The TypeScript lambda syntax is very similar to the function type declaration: we have the list of arguments (none in this particular case) in parentheses, then =>, and then the body of the function. If the function had multiple lines, we would’ve put them between { and }, but because we have only a single call to new Car(), this is implicitly considered to be the return statement for the lambda, so we get rid of makeCar() and put the construction logic in a one-liner.
lambda 或匿名函数是没有名称的函数定义。Lambda 通常用于一次性的、短暂的处理,并像数据一样传递。
A lambda, or anonymous function, is a function definition that doesn’t have a name. Lambdas are usually used for one-off, short-lived processing and are passed around like data.
如果我们不能输入函数,Lambdas 就不会很有用。我们将如何处理诸如这样的表达式() => new Car()?如果我们不能将它存储在一个变量中或将它作为参数传递给另一个函数,那么它真的没有多大用处。另一方面,具有像值一样传递函数的能力可以实现像前面那样的场景,在这种情况下,Car懒惰地生成一个实例只比急切的版本长几个字符。
Lambdas wouldn’t be very useful if we were unable to type functions. What would we do with an expression such as () => new Car()? If we couldn’t store it in a variable or pass it as an argument to another function, there really wouldn’t be much use for it. On the other hand, having the ability to pass functions around like values enables scenarios like the preceding one, in which producing a Car instance lazily is just a few characters longer than the eager version.
许多函数式编程语言的一个共同特征是惰性求值。在这样的语言中,一切都尽可能晚地评估,我们不必明确说明。在这样的语言中, chooseMyRide()默认情况下既不会构造 aBike也不会构造 a Car。只有当我们实际尝试使用返回的对象时chooseMyRide()——ride()例如,通过调用它——才会创建 Bikeor 。Car
A common feature of many functional programming languages is lazy evaluation. In such languages, everything is evaluated as late as possible, and we don’t have to be explicit about it. In such languages, chooseMyRide() would by default construct neither a Bike nor a Car. Only when we actually try to use the object returned by chooseMyRide()—by calling ride() on it, for example—would the Bike or Car be created.
TypeScript、Java、C# 和 C++ 等命令式编程语言受到热切的评估。话虽这么说,正如我们之前看到的,我们可以在必要时相当容易地模拟惰性求值。当我们稍后讨论生成器时,我们会看到更多这样的例子。
Imperative programming languages such as TypeScript, Java, C#, and C++ are eagerly evaluated. That being said, as we saw previously, we can simulate lazy evaluation fairly easily when necessary. We’ll see more examples of this when we discuss generators later.
以下哪项实现了将两个数字相加的 lambda?
- function add(x: number, y: number)=> number { return x + y; }
- add(x: number, y: number) => number { return x + y; }
- add(x: number, y: number) { return x + y; }
- (x: number, y: number) => x + y;
Which of the following implements a lambda that adds two numbers?
- function add(x: number, y: number)=> number { return x + y; }
- add(x: number, y: number) => number { return x + y; }
- add(x: number, y: number) { return x + y; }
- (x: number, y: number) => x + y;
让我们看一下类型化函数解锁的另一种能力:接受参数或返回其他函数的函数。接受一个或多个非函数参数并返回非函数类型的“普通”函数也称为一阶函数,或常规的普通函数。将一阶函数作为参数或返回一阶函数的函数称为二阶函数。
Let’s look at another capability unlocked by typed functions: functions that take as arguments or return other functions. A “normal” function that accepts one or more nonfunction arguments and returns a nonfunction type is also known as a first-order function, or a regular, run-of-the-mill function. A function that takes a first-order function as an argument or returns a first-order function is called a second-order function.
我们可以往上爬,把一个以二阶函数为参数或返回二阶函数的函数称为三阶函数,但实际上,我们只是指所有以二阶函数为参数或返回的函数其他函数作为高阶函数。
We could climb up the ladder and say that a function that takes a second-order function as an argument or returns a second-order function is called a third-order function, but in practice, we simply refer to all functions that take or return other functions as higher-order functions.
高阶函数的一个例子是chooseMyRide()上一节中的第二次迭代。该函数需要一个 type 的参数() => Car,它本身就是一个函数。
An example of a higher-order function is the second iteration of chooseMyRide() from the preceding section. That function requires an argument of type () => Car, which would be a function itself.
事实上,事实证明,几个非常有用的算法可以实现为高阶函数,最基本的是map()、filter()和reduce()。大多数编程语言都附带提供这些函数版本的库,但以 DIY 方式,我们将研究可能的实现并检查细节。
In fact, it turns out that several very useful algorithms can be implemented as higher-order functions, the most fundamental ones being map(), filter(), and reduce(). Most programming languages ship with libraries that provide versions of these functions, but in DIY fashion, we’ll look at possible implementations and go over the details.
背后的前提map()非常简单:给定某种类型的值集合,对每个值调用一个函数,然后返回结果集合。这种类型的处理在实践中反复出现,因此减少代码重复是有意义的。
The premise behind map() is very straightforward: given a collection of values of some type, call a function on each of those values, and return the collection of results. This type of processing shows up over and over in practice, so it makes sense to reduce code duplication.
我们以两个场景为例。首先,我们有一个数字数组,我们想要对数组中的每个数字进行平方。其次,我们有一个字符串数组,我们想要计算数组中每个字符串的长度。
Let’s take two scenarios as examples. First, we have an array of numbers, and we want to square each number in the array. Second, we have an array of strings, and we want to compute the length of each string in the array.
我们可以用几个for循环来实现这些示例,但是并排查看它们(如下一个清单所示)应该会给我们一种感觉,即一些共性可以抽象到共享代码中。
We could implement these examples with a couple of for loops, but looking at them side by side, as shown in the next listing, should give us a feeling that some of the commonality could be abstracted away into shared code.
让数字:数字[] = [1, 2, 3, 4, 5]; 1个
让正方形:数字[] = [];
对于(常数 n 个数字){
squares.push(n * n); 2个
}
让字符串:string[] = ["apple", "orange", "peach"]; 3个
让长度:数字[] = [];
for (const s of strings) {
lengths.push(s.length); 4
}let numbers: number[] = [1, 2, 3, 4, 5]; 1
let squares: number[] = [];
for (const n of numbers) {
squares.push(n * n); 2
}
let strings: string[] = ["apple", "orange", "peach"]; 3
let lengths: number[] = [];
for (const s of strings) {
lengths.push(s.length); 4
}
虽然数组和转换不同,但结构显然非常相似(图 5.5)。
Although the arrays and transformations are different, the structure is obviously very similar (figure 5.5).
让我们看一下 for arrays 的实现map(),看看我们如何避免一遍又一遍地编写这种循环。我们将使用通用类型Tand U,因为无论Tand是什么,实现都有效U。这样,我们可以将此函数用于不同类型,而不是将其限制为数字数组。
Let’s look at an implementation of map() for arrays and see how we can avoid writing this kind of loop over and over. We’ll use generic types T and U, as the implementation works regardless of what T and U are. This way, we can use this function with different types, as opposed to restricting it to, say, arrays of numbers.
我们的函数接受一个 s 数组T和一个接受一个项目T作为参数并返回 type 值的函数U。它将结果收集到一个Us 数组中。下一个清单中的实现简单地遍历 s 数组中的每一项,将给定的函数应用于它,然后将结果存储在s T数组中。U
Our function takes an array of Ts and a function that takes an item T as argument and returns a value of type U. It collects the result in an array of Us. The implementation in the next listing simply goes over each item in the array of Ts, applies the given function to it, and then stores the result in the array of Us.
function map<T, U>(items: T[], func: (item: T) => U): U[] { 1 let
result: U[] = []; 2个
对于(项目的常量项目){
结果.推送(功能(项目)); 3个
}
返回结果; 4
}function map<T, U>(items: T[], func: (item: T) => U): U[] { 1
let result: U[] = []; 2
for (const item of items) {
result.push(func(item)); 3
}
return result; 4
}
这个简单的函数封装了前面例子的公共处理。使用map(),我们可以使用一对单行代码生成正方形数组和字符串长度数组,如以下清单所示。
This simple function encapsulates the common processing of the preceding example. With map(), we can produce the array of squares and the array of string lengths with a couple of one-liners, as the following listing shows.
让数字:数字[] = [1, 2, 3, 4, 5]; 设正方形:number[] = map(numbers, (item) => item * item); 1个 让字符串:string[] = ["apple", "orange", "peach"]; 让长度:number[] = map(strings, (item) => item.length); 2个
let numbers: number[] = [1, 2, 3, 4, 5]; let squares: number[] = map(numbers, (item) => item * item); 1 let strings: string[] = ["apple", "orange", "peach"]; let lengths: number[] = map(strings, (item) => item.length); 2
map()封装我们作为参数给它的函数的应用程序。我们只需将一个项目数组和一个函数传递给它,然后我们取回应用该函数的结果数组。稍后,当我们讨论泛型时,我们将看到如何进一步推广它以使其适用于任何数据结构,而不仅仅是数组。但是,即使使用当前的实现,我们也可以获得一个非常好的抽象,可以将函数应用于项目集,我们可以在许多情况下重用它。
map() encapsulates the application of the function that we give it as argument. We just hand it an array of items and a function, and we get back the array resulting from the application of the function. Later, when we discuss generics, we’ll see how we can generalize this even further to make it work with any data structure, not only arrays. Even with the current implementation, though, we get a very good abstraction for applying functions to sets of items, which we can reuse in many situations.
下一个非常常见的场景map()是的表亲filter()。给定项目集合和条件,过滤掉不满足条件的项目并返回满足条件的项目集合。
The next very common scenario, the cousin of map(), is filter(). Given a collection of items and a condition, filter out the items that don’t meet the condition and return the collection of items that do.
回到我们的数字和字符串示例,让我们过滤列表,以便我们只保留偶数和 length 的字符串5。map()在这里不能帮助我们,因为它处理集合中的所有元素,但在这种情况下,我们想丢弃一些。临时实现将再次包括遍历集合并检查是否满足条件,如下一个清单所示。
Going back to our numbers and strings examples, let’s filter the list so that we keep only the even numbers and the strings of length 5. map() can’t help us here, as it processes all elements in the collection, but in this case, we want to discard some. The ad-hoc implementation would again consist of looping over the collections and checking whether the condition is met, as shown in the next listing.
让数字:数字[] = [1, 2, 3, 4, 5];
让均匀:数字[] = []
对于(常数 n 个数字){
如果 (n % 2 == 0) { 1
evens.push(n);
}
}
让字符串:string[] = ["apple", "orange", "peach"];
让 length5Strings: string[] = [];
for (const s of strings) {
如果(s.length == 5){ 2
length5Strings.push(s);
}
}let numbers: number[] = [1, 2, 3, 4, 5];
let evens: number[] = []
for (const n of numbers) {
if (n % 2 == 0) { 1
evens.push(n);
}
}
let strings: string[] = ["apple", "orange", "peach"];
let length5Strings: string[] = [];
for (const s of strings) {
if (s.length == 5) { 2
length5Strings.push(s);
}
}
同样,我们立即看到了一个共同的底层结构(图 5.6)。
Again, we immediately see a common underlying structure (figure 5.6).
就像我们对 所做的那样map(),我们可以实现一个通用的filter()高阶函数,它将一个输入数组和一个过滤器函数作为参数,并返回过滤后的输出,如以下代码所示。在这种情况下,如果输入数组的类型为T,则过滤函数是一个以 aT作为参数并返回 a 的函数boolean。接受单个参数并返回 a 的函数boolean也称为谓词。
Just as we did with map(), we can implement a generic filter() higher-order function that takes as arguments an input array and a filter function, and returns the filtered output, as shown in the following code. In this case, if the input array is of type T, the filter function is a function that takes a T as argument and returns a boolean. A function that takes a single argument and returns a boolean is also called a predicate.
function filter<T>(items: T[], pred: (item: T) => boolean): T[] { 1
让结果:T[] = [];
对于(项目的常量项目){
如果(预测(项目)){ 2
结果.推送(项目);
}
}
返回结果;
}function filter<T>(items: T[], pred: (item: T) => boolean): T[] { 1
let result: T[] = [];
for (const item of items) {
if (pred(item)) { 2
result.push(item);
}
}
return result;
}
让我们看看当我们使用我们在函数中实现的通用结构时过滤代码是什么样的filter()。偶数和长度的字符串都5成为下一个列表中的一行。
Let’s see what the filtering code looks like when we use the common structure that we implemented in our filter() function. Both the even numbers and strings of length 5 become one-liners in the next listing.
让数字:数字[] = [1, 2, 3, 4, 5]; 让 evens: number[] = filter(numbers, (item) => item % 2 == 0); 让字符串:string[] = ["apple", "orange", "peach"]; 让 length5Strings: string[] = filter(strings, (item) => item.length == 5);
let numbers: number[] = [1, 2, 3, 4, 5]; let evens: number[] = filter(numbers, (item) => item % 2 == 0); let strings: string[] = ["apple", "orange", "peach"]; let length5Strings: string[] = filter(strings, (item) => item.length == 5);
数组通过使用谓词进行过滤——在第一种情况下,true如果数字可以被 2 整除,则返回一个 lambda;在第二种情况下,true如果字符串具有长度,则返回一个 lambda 5。
The arrays are filtered by using a predicate—in the first case, a lambda that returns true if the number is divisible by 2, and in the second case, a lambda that returns true if the string has length 5.
将第二个常用操作实现为泛型函数后,让我们继续讨论本章介绍的第三个也是最后一个操作。
With the second common operation implemented as a generic function, let’s move on to the third and last operation covered in this chapter.
到目前为止,我们可以使用 将函数应用于项目集合map(),我们可以使用 将不符合特定条件的项目从集合中移除filter()。第三个常见操作涉及将所有集合项合并为一个值。
So far, we can apply a function to a collection of items by using map(), and we can remove items that don’t meet certain criteria from a collection by using filter(). The third common operation involves merging all the collection items into a single value.
例如,我们可能想要计算数字数组中所有数字的乘积,并将字符串数组中的所有字符串连接起来形成一个大字符串。这些场景不同,但具有共同的底层结构。首先,让我们看一下临时实施。
We might want to calculate the product of all numbers in a number array, for example, and concatenate all the strings in a string array to form one big string. These scenarios are different but have a common underlying structure. First, let’s look at the ad hoc implementation.
让数字:数字[] = [1, 2, 3, 4, 5];
让产品:数量= 1; 1个
对于(常数 n 个数字){
产品=产品* n; 2个
}
让字符串:string[] = ["apple", "orange", "peach"];
让 longString: string = ""; 3个
for (const s of strings) {
longString = longString + s; 4
}let numbers: number[] = [1, 2, 3, 4, 5];
let product: number = 1; 1
for (const n of numbers) {
product = product * n; 2
}
let strings: string[] = ["apple", "orange", "peach"];
let longString: string = ""; 3
for (const s of strings) {
longString = longString + s; 4
}
在这两种情况下,我们都从一个初始值开始;然后我们通过遍历集合并将每个项目与累加器组合来累加结果。当我们遍历集合时,product包含数字数组中所有数字的乘积,并且longString是字符串数组中所有字符串的串联(图 5.7)。
In both scenarios, we start with an initial value; then we accumulate the result by going over the collections and combining each item with the accumulator. When we’re done going over the collections, product contains the product of all the numbers in the numbers array, and longString is the concatenation of all strings in the strings array (figure 5.7).
在清单 5.18中,我们将实现一个泛型函数,它接受一个 s 数组T、一个 type 的初始值T,以及一个接受两个 type 参数T并返回 a 的函数T。我们会将运行总计存储在一个局部变量中,并通过依次将函数应用于它和输入数组的每个元素来更新它。
In listing 5.18, we’ll implement a generic function that takes an array of Ts, an initial value of type T, and a function that takes two arguments of type T and returns a T. We’ll store the running total in a local variable and update it by applying the function to it and each element of the input array in turn.
function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T { 1
让结果:T = init;
对于(项目的常量项目){
结果 = 操作(结果,项目); 2个
}
返回结果;
}function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T { 1
let result: T = init;
for (const item of items) {
result = op(result, item); 2
}
return result;
}
这个函数有三个参数,其他函数有两个。我们需要一个初始值而不是从数组的第一个元素开始的原因是我们需要处理输入数组为空的情况。result如果集合中没有项目会怎样?有一个初始值涵盖了这种情况,因为我们会简单地返回它。
This function has three arguments, and the others have two. The reason why we need an initial value instead of starting with, say, the first element of the array is that we need to handle the case when the input array is empty. What would result be if there was no item in the collection? Having an initial value covers that case, as we would simply return that.
让我们看看如何更新我们的临时实现以使用reduce().
Let’s see how we can update our ad-hoc implementations to use reduce().
让数字:数字[] = [1, 2, 3, 4, 5]; 让乘积:number = reduce(numbers, 1, (x, y) => x * y); 1个 让字符串:string[] = ["apple", "orange", "peach"]; 让 longString: string = reduce(strings, "", (x, y) => x + y); 2个
let numbers: number[] = [1, 2, 3, 4, 5]; let product: number = reduce(numbers, 1, (x, y) => x * y); 1 let strings: string[] = ["apple", "orange", "peach"]; let longString: string = reduce(strings, "", (x, y) => x + y); 2
reduce()有一些其他两个函数所没有的微妙之处。除了需要初始值外,项目组合的顺序可能会影响最终结果。对于我们示例中的操作和初始值,情况并非如此。但是如果我们的初始字符串是 呢"banana"?然后,从左到右连接,我们会得到"bananaappleorangepeach".但是如果我们从右到左遍历数组,总是将项目添加到字符串的开头,我们会得到"appleorangepeachbanana"。
reduce() has a few subtleties that the other two functions don’t. Besides requiring an initial value, the order in which the items are combined may affect the final result. For the operations and initial values in our example, that’s not the case. But what if our initial string was "banana"? Then, concatenating from left to right, we would get "bananaappleorangepeach". But if we traversed the array from right to left, always adding the item to the beginning of the string, we would get "appleorangepeachbanana".
或者,如果我们的组合操作将每个字符串的第一个字母附加在一起,将其应用于"apple"and "orange"first 将得到"ao". 再次应用到"ao"并"peach"会给我们"ap"。另一方面,如果我们从"orange"和开始"peach",我们就会有"op"。然后"apple"会给"op"我们"ao"(图5.8)。
Or if our combining operation appended the first letters of each string together, applying that to "apple" and "orange" first would give us "ao". Applying it again to "ao" and "peach" would give us "ap". On the other hand, if we started with "orange" and "peach", we would have "op". Then "apple" and "op" would give us "ao" (figure 5.8).
按照惯例,reduce()是从左到右应用的,因此无论何时您遇到它作为库函数时,都应该安全地假设它是如何工作的。一些图书馆还提供从右到左的版本。Array例如,JavaScript类型同时具有reduce()和reduceRight()方法。如果您想了解更多关于这背后的数学知识,请参阅边栏“Monoids”。
Conventionally, reduce() is applied left to right, so whenever you encounter it as a library function, it should be safe to assume that’s how it works. Some libraries also provide a right-to-left version. The JavaScript Array type, for example, has both reduce() and reduceRight() methods. See the sidebar “Monoids” if you want to learn more about the math behind this.
在抽象代数中,我们处理集合和对这些集合的操作。正如我们之前看到的,我们可以将类型视为一组可能的值。T对接受两个Ts 并返回另一个T,的 type 的操作(T, T) => T可以解释为对值集的操作T。number和的集合+,例如(x, y) => x + y,形成一个代数结构。
In abstract algebra, we deal with sets and operations on those sets. As we saw previously, we can think of a type as a set of possible values. An operation on type T that takes two Ts and returns another T, (T, T) => T, can be interpreted as an operation on the set of values T. The set of number and +, which is (x, y) => x + y, for example, forms an algebraic structure.
这些结构由它们的操作属性定义。标识是操作所针对的元素。换句话说,与任何其他元素组合会使其他元素保持不变。恒等式是集合为加法时,集合为乘法时为恒等式,集合为字符串拼接时为(空字符串)。 idTop(x, id) == op(id, x) == xid0number1number""string
These structures are defined by the properties of their operations. An identity is an element id of T for which the operation op(x, id) == op(id, x) == x. In other words, combining id with any other element leaves the other element unchanged. Identity is 0 when the set is number and the operation is addition, 1 when the set is number and the operation is multiplication, and "" (the empty string) when the set is string and the operation is string concatenation.
关联性是操作的一个属性,表示我们将它应用于元素序列的顺序无关紧要,因为我们最终会得到相同的结果。对于任何x, y, zof T, op(x, op(y, z)) == op(op(x, y), z)。例如,对于数字加法和乘法是正确的,但对于减法或我们的“两个字符串的第一个字母”操作则不是这样。
Associativity is a property of the operation that says the order in which we apply it to a sequence of elements doesn’t matter, as we’ll get the same result in the end. For any x, y, z of T, op(x, op(y, z)) == op(op(x, y), z). This is true, for example, for number addition and multiplication but not true for subtraction or our “first letter of both strings” operation.
如果T具有操作的集合op具有标识元素并且该操作是关联的,则生成的代数结构称为monoid。对于幺半群,以身份作为初始值,从左到右或从右到左减少会产生相同的结果。如果集合为空,我们甚至可以删除对初始值和默认标识的要求。我们也可以并行化归约。我们可以并行减少集合的前半部分和后半部分并组合结果,例如,因为关联性属性保证我们会得到相同的结果。对于[1, 2, 3, 4, 5, 6],我们可以并行组合1 + 2 + 3和4 + 5 + 6,然后将结果相加。
If a set T with an operation op has an identity element and the operation is associative, the resulting algebraic structure is called a monoid. For a monoid, starting with the identity as the initial value, reducing from left to right or right to left yields the same result. We can even remove the requirement for an initial value and default to the identity if the collection is empty. We can also parallelize reduction. We could reduce the first half and the second half of the collection in parallel and combine the results, for example, because the associativity property guarantees that we’ll get the same result. For [1, 2, 3, 4, 5, 6], we can combine 1 + 2 + 3 and, in parallel, 4 + 5 + 6, and then add the results together.
一旦我们放弃其中一个属性,我们就失去了这些保证。如果我们没有结合性,只有一个集合、一个操作和一个单位元素,虽然我们仍然不需要初始值(我们使用单位元素),但我们应用操作的方向变得很重要。如果我们删除标识元素但保持结合性,我们就有一个半群。没有身份,我们将初始值放在第一个元素的左侧还是最后一个元素的右侧很重要。
As soon as we drop one of the properties, we lose these guarantees. If we don’t have associativity, but just a set, an operation, and an identity element, although we still don’t require an initial value (we use the identity element), the direction in which we apply the operations becomes important. If we drop the identity element but keep associativity, we have a semigroup. Without an identity, it matters whether we put the initial value on the left of the first element or the right of the last element.
关键要点是reduce()在幺半群上无缝工作,但如果我们没有幺半群,我们应该小心我们使用什么作为我们的初始值和我们减少的方向。
The key takeaway is that reduce() works seamlessly on a monoid, but if we don’t have a monoid, we should be careful what we use for our initial value and the direction we’re reducing on.
如本节开头所述,大多数编程语言都具有对这些常用算法的库支持。但是,它们可能会以不同的名称出现,因为没有为它们命名的黄金标准。
As mentioned at the start of this section, most programming languages have library support for these common algorithms. They may show up under different names, though, as there is no golden standard for naming them.
在 C# 中,map()、filter()和reduce()在命名空间中分别显示System.Linq为Select()、Where()和Aggregate()。在 Java 中,它们显示为map()、filter()和reduce()in java.util.stream。
In C#, map(), filter(), and reduce() show up in the System.Linq namespace as Select(), Where(), and Aggregate() respectively. In Java, they show up as map(), filter(), and reduce() in java.util.stream.
map()也称为Select()或transform()。filter()也被称为Where(). reduce()也称为accumulate()、Aggregate()或fold(),具体取决于语言和库。
map() is also known as Select() or transform(). filter() is also known as Where(). reduce() is also known as accumulate(), Aggregate(), or fold(), depending on the language and library.
尽管它们有很多名称,但这些算法在广泛的应用程序中都是基础和有用的。我们将在本书后面讨论许多类似的算法,但这三种算法构成了使用高阶函数进行数据处理的基础。
Even though they have many names, these algorithms are fundamental and useful across a broad range of applications. We’ll discuss many similar algorithms later in the book, but these three form the foundation of data processing using higher-order functions.
Google 著名的 MapReduce 大规模数据处理框架通过在多个节点上运行大规模并行操作并通过类操作组合结果,使用与map()和算法相同的基本原理。 reduce()map()reduce()
Google’s famous MapReduce large-scale data processing framework uses the same underlying principles of the map() and reduce() algorithms by running a massively parallel map() operation on multiple nodes and combining the results via a reduce()-like operation.
实现一个first()接受 s 数组的函数和一个接受 a作为参数并返回 a 的T函数(用于谓词) 。将返回返回的数组的第一个元素,或者如果返回所有元素。 predTbooleanfirst()pred()trueundefinedpred()false
Implement a first() function that takes an array of Ts and a function pred (for predicate) that takes a T as an argument and returns a boolean. first() will return the first element of the array for which pred() returns true or undefined if pred() returns false for all elements.
实现一个all()接受 s 数组的函数和一个接受 a作为参数并返回 a 的T函数(用于谓词) 。如果是数组的所有元素,将返回 true ;否则,它将返回。 predTbooleanall()pred()truefalse
Implement an all() function that takes an array of Ts and a function pred (for predicate) that takes a T as an argument and returns a boolean. all() will return true if pred() is true for all the elements of the array; otherwise, it will return false.
尽管本章涵盖的材料有点复杂,但好消息是我们已经了解了函数式编程的大部分关键要素。如果您习惯于命令式、面向对象的语言,某些函数式语言的语法可能会令人反感。他们的类型系统通常提供和类型、乘积类型和一阶函数支持的一些组合,以及一组库函数,例如 、map()和filter()来reduce()处理数据。许多函数式语言使用惰性求值,我们也在本章中讨论过。
Although the material covered in this chapter was a bit more complex, the good news is that we went over most of the key ingredients of functional programming. The syntax of some functional languages may be off-putting if you’re used to imperative, object-oriented languages. Their type systems usually offer some combinations of sum types, product types, and first-order function support, as well as a set of library functions such as map(), filter(), and reduce() to process data. Many functional languages employ lazy evaluation, which we also discussed in this chapter.
有了对函数进行类型化的能力,许多源自函数式编程语言的概念可以用非函数式(或纯函数式)的语言来实现。我们在本章中看到了这一点;我们触及了所有这些主题,并为所有这些关键组件提供了必要的实现。
With the ability to type functions, many of the concepts originating from functional programming languages can be implemented in languages that aren’t functional (or purely functional). We saw this throughout this chapter; we touched on all these topics and provided imperative implementations for all these key components.
在第 6 章中,我们将研究类型化函数的更多应用。我们将了解闭包以及如何使用它们来简化另一种常见的设计模式:装饰器模式。我们还将讨论承诺以及任务分配和事件驱动系统。所有这些应用程序都可以通过将计算(函数)表示为类型系统的一等公民的能力来实现。
In chapter 6, we’ll look at a few more applications of typed functions. We’ll learn about closures and how we can use them to simplify another common design pattern: the decorator pattern. We’ll also talk about promises, as well as tasking and event-driven systems. All these applications are made possible by the ability to represent computation (functions) as first-class citizens of the type system.
b—那是唯一的函数类型;其他声明不代表功能。
b—That is the only function type; the other declarations do not represent functions.
c—该函数接受 anumber和 an(x: number) => boolean并返回boolean。
c—The function takes a number and an (x: number) => boolean and returns boolean.
我们可以将连接建模为具有两个状态的状态机——open和closed——以及两个转换——从到的connect转换和从到的断开转换。 closedopenopenclosed
We can model the connection as a state machine with two states—open and closed—and two transitions—connect transitions from closed to open and disconnect transitions from open to closed.
一个可能的实现:
声明函数 read(): string; 类连接{ 私有 doProcess: () => void = this.processClosedConnection; 公共过程():无效{ 这个.doProcess(); } 私有 processClosedConnection() { this.doProcess = this.processOpenConnection; } 私人进程OpenConnection(){ 常量值:string = read(); 如果(值。长度== 0){ this.doProcess = this.processClosedConnection; } 别的 { 控制台日志(值); } } }
A possible implementation:
declare function read(): string; class Connection { private doProcess: () => void = this.processClosedConnection; public process(): void { this.doProcess(); } private processClosedConnection() { this.doProcess = this.processOpenConnection; } private processOpenConnection() { const value: string = read(); if (value.length == 0) { this.doProcess = this.processClosedConnection; } else { console.log(value); } } }
d——其他实现命名函数;这是唯一的匿名实现。
d—The other implement named functions; this is the only anonymous implementation.
一个可能的实现first():
函数 first<T>(items: T[], pred: (item: T) => boolean): 吨 | 不明确的 { 对于(项目的常量项目){ 如果(预测(项目)){ 归还物品; } } 返回未定义; }
A possible implementation for first():
function first<T>(items: T[], pred: (item: T) => boolean): T | undefined { for (const item of items) { if (pred(item)) { return item; } } return undefined; }
一个可能的实现all():
函数 all<T>(items: T[], pred: (item: T) => boolean): boolean { 对于(项目的常量项目){ 如果(!pred(项目)){ 返回假; } } 返回真; }
A possible implementation for all():
function all<T>(items: T[], pred: (item: T) => boolean): boolean { for (const item of items) { if (!pred(item)) { return false; } } return true; }
本章涵盖
This chapter covers
在第 5 章中,我们介绍了函数类型的基础知识以及通过将函数作为参数传递并将它们作为结果返回而像对待其他值一样对待函数的能力所支持的场景。我们还研究了一些实现常见数据处理模式的强大抽象:map()、filter()和reduce()。
In chapter 5, we covered the basics of function types and scenarios enabled by the ability to treat functions like other values by passing them as arguments and returning them as results. We also looked at some powerful abstractions that implement common data processing patterns: map(), filter(), and reduce().
在本章中,我们将通过一些更高级的应用程序继续讨论函数类型。我们将从查看装饰器模式、它的书本实现和替代实现开始。(再次强调,如果您忘记了它,请不要担心;我们会快速复习一下。)我们将介绍闭包的概念,看看我们如何使用它来实现一个简单的计数器。然后我们将看看另一种实现计数器的方法,这次使用生成器:一个产生多个结果的函数。
In this chapter, we’ll continue our discussion of function types with some more advanced applications. We’ll start by looking at the decorator pattern, its by-the-book implementation, and an alternative implementation. (Again, don’t worry if you forgot it; we’ll have a quick refresher.) We’ll introduce the concept of a closure and see how we can use it to implement a simple counter. Then we’ll look at another way to implement a counter, this time with a generator: a function that yields multiple results.
接下来,我们将讨论异步操作。我们将回顾两个主要的异步执行模型——线程和事件循环——并看看我们如何对几个长时间运行的操作进行排序。我们将从回调开始;然后我们将看看承诺,最后,我们将介绍当今大多数主流编程语言提供的async/await语法。
Next, we’ll talk about asynchronous operations. We’ll go over the two main asynchronous execution models—threads and event loops—and look at how we can sequence several long-running operations. We’ll start with callbacks; then we’ll look at promises, and finally, we’ll cover the async/await syntax provided nowadays by most mainstream programming languages.
本章讨论的所有主题之所以成为可能,是因为我们可以将函数用作值,正如我们将在接下来的几页中看到的那样。
All the topics discussed in this chapter are made possible because we can use functions as values, as we’ll see in the following pages.
装饰者模式是一种行为软件设计模式,它在不修改对象的类的情况下扩展对象的行为。装饰对象可以执行超出其原始实现所提供的工作。该模式如图6.1所示。
The decorator pattern is a behavioral software design pattern that extends the behavior of an object without modifying the class of the object. A decorated object can perform work beyond what its original implementation provides. The pattern looks like figure 6.1.
作为一个例子,假设我们有一个IWidgetFactory声明一个make-Widget()方法返回一个的Widget。具体实现,Widget-Factory,实现实例化新对象的方法Widget。
As an example, suppose that we have an IWidgetFactory that declares a make-Widget() method returning a Widget. The concrete implementation, Widget-Factory, implements the method to instantiate new Widget objects.
假设我们想要重用 a Widget,那么我们不想总是创建一个新的,而是只想创建一个并不断返回它(也就是说,有一个单例)。没有 修改我们的WidgetFactory,我们可以创建一个名为 的装饰器Singleton-Decorator,它包装一个IWidgetFactory,如下一个清单所示,并扩展其行为以确保只Widget创建一个(图 6.2)。
Suppose that we want to reuse a Widget, so instead of always creating a new one, we want to create just one and keep returning it (that is, have a singleton). Without modifying our WidgetFactory, we can create a decorator called Singleton-Decorator, which wraps an IWidgetFactory, as shown in the next listing, and extends its behavior to ensure that only a single Widget gets created (figure 6.2).
类小部件 { }
接口 IWidgetFactory {
makeWidget(): 小部件;
}
类 WidgetFactory 实现 IWidgetFactory {
公共 makeWidget(): 小部件 {
返回新的小部件(); 1个
}
}
类 SingletonDecorator 实现 IWidgetFactory {
私有工厂:IWidgetFactory; 2个
私有实例:小部件 | 未定义=未定义;
构造函数(工厂:IWidgetFactory){
这个.工厂=工厂;
}
公共 makeWidget(): 小部件 {
if (this.instance == undefined) { 3
this.instance = this.factory.makeWidget(); 3个
}
返回这个实例;
}
}class Widget { }
interface IWidgetFactory {
makeWidget(): Widget;
}
class WidgetFactory implements IWidgetFactory {
public makeWidget(): Widget {
return new Widget(); 1
}
}
class SingletonDecorator implements IWidgetFactory {
private factory: IWidgetFactory; 2
private instance: Widget | undefined = undefined;
constructor(factory: IWidgetFactory) {
this.factory = factory;
}
public makeWidget(): Widget {
if (this.instance == undefined) { 3
this.instance = this.factory.makeWidget(); 3
}
return this.instance;
}
}
使用这种模式的好处是它支持单一职责原则,即一个类应该只有一个职责。在这种情况下,Widget-Factory负责创建小部件,而SingletonDecorator负责单例行为。如果我们想要多个实例,我们直接使用Widget-Factory。如果我们想要单个实例,我们使用SingletonDecorator.
The advantage of using this pattern is that it supports the single-responsibility principle, which says that a class should have just one responsibility. In this case, the Widget-Factory is responsible for creating widgets, whereas the SingletonDecorator is responsible for the singleton behavior. If we want multiple instances, we use the Widget-Factory directly. If we want a single instance, we use SingletonDecorator.
让我们看看如何再次使用类型化函数来简化此实现。首先,让我们摆脱IWidgetFactory接口并用函数类型替换它。Widget那将是不带参数并返回:的函数类型 () => Widget。
Let’s see how we can simplify this implementation, again by using typed functions. First, let’s get rid of the IWidgetFactory interface and replace it with a function type. That would be the type of a function that takes no arguments and returns a Widget: () => Widget.
现在我们可以WidgetFactory用一个简单的函数替换我们的类,make-Widget(). 每当我们使用IWidgetFactory之前,传入一个实例时WidgetFactory,我们现在需要一个类型的函数() => Widget并传入makeWidget(),如以下清单所示。
Now we can replace our WidgetFactory class with a simple function, make-Widget(). Whenever we would’ve used an IWidgetFactory before, passing in an instance of WidgetFactory, we now require a function of type () => Widget and pass in makeWidget(), as the following listing shows.
类小部件 { }
输入 WidgetFactory = () => 小部件; 1个
函数 makeWidget(): 小部件 { 2
返回新的小部件();
}
函数 use10Widgets(工厂:WidgetFactory){ 3
对于(设 i = 0;i < 10;i++){
让小部件=工厂();
/* ... */
}
}
使用 10Widgets(makeWidget); 4个class Widget { }
type WidgetFactory = () => Widget; 1
function makeWidget(): Widget { 2
return new Widget();
}
function use10Widgets(factory: WidgetFactory) { 3
for (let i = 0; i < 10; i++) {
let widget = factory();
/* ... */
}
}
use10Widgets(makeWidget); 4
对于功能部件工厂,我们使用了一种与第 5 章中的策略模式非常相似的技术:我们获取一个函数作为参数,并在需要时调用它。现在让我们看看如何添加单例行为。
With the functional widget factory, we use a technique very similar to the strategy pattern in chapter 5: we get a function as an argument and call it when needed. Now let’s see how we can add the singleton behavior.
我们提供了一个新函数 ,singletonDecorator()它接受一个Widget-Factory-type 函数并返回另一个WidgetFactory-type 函数。请记住第 5 章,lambda 是一个没有名字的函数,我们可以从另一个函数返回它。在下一个清单中,我们的装饰器将使用一个工厂并使用它来构建一个处理单例行为的新函数(图 6.3)。
We provide a new function, singletonDecorator(), that takes a Widget-Factory-type function and returns another WidgetFactory-type function. Remember from chapter 5 that a lambda is a function without a name, which we can return from another function. In the next listing, our decorator will take a factory and use it to build a new function that handles the singleton behavior (figure 6.3).
类小部件 { }
输入 WidgetFactory = () => 小部件;
函数 makeWidget(): 小部件 {
返回新的小部件();
}
function singletonDecorator(factory: WidgetFactory): WidgetFactory {
让实例: Widget | 未定义=未定义;
return (): Widget => { 1
if (instance == undefined) {
instance = factory();
}
返回实例;
};
}
函数 use10Widgets(工厂:WidgetFactory){
对于(设 i = 0;i < 10;i++){
让小部件=工厂();
/* ... */
}
}
使用 10Widgets( singletonDecorator(makeWidget) ); 2个class Widget { }
type WidgetFactory = () => Widget;
function makeWidget(): Widget {
return new Widget();
}
function singletonDecorator(factory: WidgetFactory): WidgetFactory {
let instance: Widget | undefined = undefined;
return (): Widget => { 1
if (instance == undefined) {
instance = factory();
}
return instance;
};
}
function use10Widgets(factory: WidgetFactory) {
for (let i = 0; i < 10; i++) {
let widget = factory();
/* ... */
}
}
use10Widgets(singletonDecorator(makeWidget)); 2
现在,将调用 lambda ,而不是构造 10 个Widget对象,它将为所有调用重用相同的实例。 use10Widgets()Widget
Now, instead of constructing 10 Widget objects, use10Widgets() will call the lambda, which will reuse the same Widget instance for all calls.
这段代码减少了组件的数量,从一个接口和两个类,每个类都有一个方法(具体操作和装饰器)到两个函数。
This code reduces the number of components from an interface and two classes, each with a method (the concrete operation and the decorator) to two functions.
与我们的策略模式一样,面向对象和函数式方法实现了相同的装饰器模式。面向对象的版本需要一个接口声明 ( IWidgetFactory),该接口的至少一个实现 ( Widget-Factory),以及一个处理添加的行为的装饰器类 ( Singleton-Decorator)。相比之下,函数式实现仅声明工厂函数 () 的类型() => Widget并使用两个函数:工厂函数 ( makeWidget()) 和装饰器函数 ( singletonDecorator())。
As with our strategy pattern, the object-oriented and functional approaches implement the same decorator pattern. The object-oriented version requires an interface declaration (IWidgetFactory), at least one implementation of that interface (Widget-Factory), and a decorator class that handles the added behavior (Singleton-Decorator). By contrast, the functional implementation simply declares the type of the factory function (() => Widget) and uses two functions: a factory function (makeWidget()) and a decorator function (singletonDecorator()).
需要注意的一件事是,在功能情况下,装饰器的类型与makeWidget(). 工厂不期望任何参数并返回 a Widget,而装饰器采用一个小部件工厂并返回另一个小部件工厂。换句话说,singletonDecorator()接受一个函数作为参数并返回一个函数作为它的结果。如果没有一流的函数,这是不可能的:将函数视为任何其他变量并将它们用作参数和返回值的能力。
One thing to note is that in the functional case, the decorator does not have the same type as makeWidget(). Whereas the factory doesn’t expect any arguments and returns a Widget, the decorator takes a widget factory and returns another widget factory. In other words, singletonDecorator() takes a function as an argument and returns a function as its result. This wouldn’t be possible without first-class functions: the ability to treat functions as any other variables and use them as arguments and return values.
现代类型系统支持的更简洁的实现适用于许多情况。当我们处理多个函数时,我们可以使用更详细的面向对象解决方案。如果我们的接口声明了几个方法,我们就不能用一个单一的函数类型来替换它。
The more-succinct implementation, enabled by modern type systems, is good for many situations. We can use the more-verbose object-oriented solution when we are dealing with more than a single function. If our interface declares several methods, we can’t replace it with a single function type.
让我们放大清单 6.4singletonDecorator()中的实现。您可能已经注意到了一些有趣的事情:尽管函数返回一个 lambda,但 lambda 引用了 argument和 variable ,它们应该是函数的本地变量。 factoryinstancesingletonDecorator()
Let’s zoom in on the singletonDecorator() implementation in listing 6.4. You may have noticed something interesting: even though the function returns a lambda, the lambda references both the factory argument and the variable instance, which should be local to the singletonDecorator() function.
函数 singletonDecorator(工厂:WidgetFactory):WidgetFactory {
让实例:小部件 | 未定义=未定义;
返回():小部件=> {
如果(实例==未定义){
实例=工厂();
}
返回实例;
};
}function singletonDecorator(factory: WidgetFactory): WidgetFactory {
let instance: Widget | undefined = undefined;
return (): Widget => {
if (instance == undefined) {
instance = factory();
}
return instance;
};
}
即使在我们从 返回之后singletonDecorator(),该instance变量仍然存在,因为它被 lambda“捕获”了,这被称为lambda 捕获。
Even after we return from singletonDecorator(), the instance variable is still alive, as it was “captured” by the lambda, which is known as a lambda capture.
lambda 捕获是在 lambda 中捕获的外部变量。编程语言通过闭包实现 lambda 捕获。闭包不仅仅是一个简单的函数:它还记录了创建函数的环境,因此它可以在调用之间保持状态。
A lambda capture is an external variable captured within a lambda. Programming languages implement lambda captures through closures. A closure is something more than a simple function: it also records the environment in which the function was created, so it can maintain state between calls.
在我们的例子中,instance变量 insingletonDecorator()是该环境的一部分。我们返回的 lambda 仍然可以引用instance(图 6.4)。
In our case, the instance variable in singletonDecorator() is part of that environment. The lambda we return will still be able to reference instance (figure 6.4).
只有当我们有高阶函数时,闭包才有意义。如果我们不能从另一个函数返回一个函数,就没有环境可以捕获。在那种情况下,所有功能都在全局范围内,这是它们的环境。他们可以引用全局变量。
Closures make sense only if we have higher-order functions. If we can’t return a function from another function, there is no environment to capture. In that case, all functions are in the global scope, which is their environment. They can reference global variables.
另一种思考闭包的方法是将它们与对象进行对比。对象用一组方法表示某种状态;闭包表示具有某些捕获状态的函数。让我们看另一个可以使用闭包的例子:实现一个计数器。
Another way to think about closures is to contrast them with objects. An object represents some state with a set of methods; a closure represents a function with some captured state. Let’s look at another example in which closures can be used: implementing a counter.
实现一个函数 ,loggingDecorator()该函数将另一个函数 作为参数,factory()该函数不接受任何参数并返回一个Widget对象。装饰给定的函数,以便无论何时调用它,它都会"Widget created"在返回Widget对象之前进行记录。
Implement a function, loggingDecorator(), that takes as argument another function, factory(), that takes no arguments and returns a Widget object. Decorate the given function so that whenever it is called, it logs "Widget created" before returning a Widget object.
让我们来看一个非常简单的场景:我们想要创建一个计数器,它为我们提供从 1 开始的连续数字。尽管这个示例可能看起来微不足道,但它涵盖了几种可能的实现,这些实现可以推广到我们需要生成值的任何场景。一种方法是使用一个全局变量和一个返回该变量然后递增的函数,如以下代码所示。
Let’s look at a very simple scenario: we want to create a counter that gives us consecutive numbers starting from 1. Although this example may seem trivial, it covers several possible implementations that generalize to any scenario in which we need to generate values. One way is to use a global variable and a function that returns that variable and then increments, as shown in the following code.
让 n: 数字 = 1; 1个
功能下一个(){
返回 n++; 2个
}
控制台日志(下一个()); 3
控制台日志(下一步()); 3
控制台日志(下一步()); 3个let n: number = 1; 1
function next() {
return n++; 2
}
console.log(next()); 3
console.log(next()); 3
console.log(next()); 3
此实现有效,但并不理想。首先,n是一个全局变量,因此任何人都可以访问它。其他代码可能会从我们下面改变它。其次,这个实现给了我们一个单一的计数器。如果我们想要两个计数器,都从 1 开始怎么办?
This implementation works, but it’s not ideal. First, n is a global variable, so anyone has access to it. Other code might change it from underneath us. Second, this implementation gives us a single counter. What if we want two counters, both starting from 1?
我们要看的第一个实现是面向对象的,应该很熟悉。我们创建一个Counter类,它将计数器的状态存储为私有成员。我们提供了一种next()方法,它返回并递增该计数器。通过这种方式,我们封装了计数器,使外部代码无法更改它,并且我们可以创建任意数量的计数器作为此类的实例。
The first implementation we will look at is an object-oriented one, which should be familiar. We create a Counter class, which stores the state of the counter as a private member. We provide a next() method, which returns and increments that counter. In this way, we encapsulate the counter so that external code can’t change it and we can create as many counters as we want as instances of this class.
类计数器 {
私人 n: 数字 = 1; 1个
下一个():数字{
返回这个.n++;
}
}
让 counter1: Counter = new Counter(); 2
让counter2:Counter = new Counter(); 2个
console.log(counter1.next()); 3
console.log(counter2.next()); 3
console.log(counter1.next()); 3
console.log(counter2.next()); 3个class Counter {
private n: number = 1; 1
next(): number {
return this.n++;
}
}
let counter1: Counter = new Counter(); 2
let counter2: Counter = new Counter(); 2
console.log(counter1.next()); 3
console.log(counter2.next()); 3
console.log(counter1.next()); 3
console.log(counter2.next()); 3
这种方法效果更好。事实上,大多数现代编程语言都为像我们的计数器这样的类型提供了一个接口,它在每次调用时都提供一个值,并有特殊的语法来迭代它。在 TypeScript 中,这是通过Iterable接口和for ... of循环完成的。我们将在本书后面讨论泛型编程时讨论这个主题。现在,我们只注意到这种模式很常见。C# 通过IEnumerable接口和foreach循环实现它,而 Java 通过Iterable接口和for : loop.
This approach works better. In fact, most modern programming languages provide an interface for types such as our counter, which provides a value on each call and has special syntax to iterate over it. In TypeScript, this is done with the Iterable interface and for ... of loop. We cover this topic later in the book, when we discuss generic programming. For now, we’ll just note that this pattern is common. C# implements it with the IEnumerable interface and the foreach loop, whereas Java does it with the Iterable interface and the for : loop.
接下来,让我们看一下利用闭包来实现计数器的功能替代方案。
Next, let’s look at a functional alternative that leverages closures to implement the counter.
makeCounter()在下一个清单中,我们将通过一个在调用时返回计数器函数的函数来实现功能计数器。我们将计数器初始化为局部变量makeCounter(),然后在返回函数中捕获它。
In the next listing, we’ll implement the functional counter through a makeCounter() function that returns a counter function when called. We will initialize the counter as a variable local to makeCounter() and then capture it in the return function.
输入 Counter = () => 数字; 1个
函数 makeCounter(): 计数器 {
让 n: 数字 = 1; 2
2
返回 () => n++; 2个
}
让 counter1: Counter = makeCounter();
让 counter2: Counter = makeCounter();
console.log(counter1()); 3
console.log(counter2()); 3
console.log(counter1()); 3
console.log(counter2()); 3个type Counter = () => number; 1
function makeCounter(): Counter {
let n: number = 1; 2
2
return () => n++; 2
}
let counter1: Counter = makeCounter();
let counter2: Counter = makeCounter();
console.log(counter1()); 3
console.log(counter2()); 3
console.log(counter1()); 3
console.log(counter2()); 3
counter1.next()现在每个计数器都是一个函数,所以我们不调用,而是简单地调用counter1()。我们还看到每个计数器捕获一个单独的值:调用counter1()不会影响counter2(),因为每当我们调用时makeCounter(),都会创建一个新值n。每个返回的函数都有自己的n. 柜台是关闭的。此外,这些值在调用之间持续存在。这种行为与函数局部变量的行为不同,函数局部变量在调用函数时创建,在函数返回时销毁(图 6.5)。
Each counter is a function now, so instead of calling counter1.next(), we simply call counter1(). We also see that each counter captures a separate value: calling counter1() does not affect counter2() because whenever we call makeCounter(), a new n gets created. Each function returned keeps its own n. The counters are closures. Also, these values persist between calls. This behavior is different from that of variables that are local to a function, which are created when the function is called and disposed of when the function returns (figure 6.5).
定义计数器的另一种方法是使用可恢复函数。面向对象的计数器通过私有成员跟踪状态。功能计数器在其捕获的上下文中跟踪状态。
Another way to define a counter is to use a resumable function. An object-oriented counter keeps track of state via a private member. A functional counter keeps track of state in its captured context.
可恢复函数是一种跟踪其自身状态的函数,无论何时被调用,都不会从头开始运行;相反,它会从上次返回时中断的地方继续执行。
A resumable function is a function that keeps track of its own state and, whenever it gets called, doesn’t run from the beginning; rather, it resumes executing from where it left off the last time it returned.
在 TypeScript 中,我们不使用关键字return来退出函数,而是使用关键字,如清单 6.8yield所示。此关键字暂停函数,将控制权交还给调用者。再次调用时,将从语句恢复执行 。 yield
In TypeScript, instead of using the keyword return to exit the function, we use the keyword yield, as shown in listing 6.8. This keyword suspends the function, giving control back to the caller. When called again, execution is resumed from the yield statement.
using 有更多限制yield:函数必须声明为生成器,并且它的返回类型必须是可迭代的迭代器。生成器通过在函数名称前加上星号来声明。
There are a couple more constraints for using yield: the function must be declared as a generator, and its return type must be an iterable iterator. A generator is declared by prefixing the function name with an asterisk.
函数* counter(): IterableIterator<number> { 1
让 n: 数字 = 1;
而(真){
产量n++; 2个
}
}
让 counter1: IterableIterator<number> = counter(); 3
让 counter2: IterableIterator<number> = counter(); 3个
console.log(counter1.next()); 4
console.log(counter2.next()); 4
console.log(counter1.next()); 4
console.log(counter2.next()); 4个function* counter(): IterableIterator<number> { 1
let n: number = 1;
while (true) {
yield n++; 2
}
}
let counter1: IterableIterator<number> = counter(); 3
let counter2: IterableIterator<number> = counter(); 3
console.log(counter1.next()); 4
console.log(counter2.next()); 4
console.log(counter1.next()); 4
console.log(counter2.next()); 4
这种实现在某种程度上是我们的面向对象和功能计数器之间的混合。计数器的实现读起来像一个函数:我们从n1 开始,然后永远循环,产生计数器值并递增它。另一方面,编译器生成的代码是面向对象的:我们的计数器实际上是一个IterableIterator<number>,我们调用next()它来获取下一个值。
This implementation is in a way a mix between our object-oriented and functional counters. The implementation of the counter reads like a function: we start with n being 1 and then loop forever, yielding the counter value and incrementing it. On the other hand, the code generated by the compiler is object-oriented: our counter is actually an IterableIterator<number>, and we call next() on it to get the next value.
即使我们用一条while (true)语句来实现它,我们也不会陷入无限循环;该函数保持产生值并在每次之后被暂停yield。在幕后,编译器将我们编写的代码翻译成看起来更像我们以前的实现的代码。
Even though we implement this with a while (true) statement, we don’t get stuck in an infinite loop; the function keeps yielding values and gets suspended after each yield. Behind the scenes, the compiler translates the code we wrote into something that looks more like our previous implementations.
这个函数的类型是() => IterableIterator<number>. 请注意,它是一个生成器这一事实不会影响它的类型。没有参数但会返回 的函数IterableIterator<number>将具有完全相同的类型。*编译器使用声明来允许语句yield,但对类型系统是透明的。
The type of this function is () => IterableIterator<number>. Notice that the fact that it is a generator doesn’t affect its type. A function with no arguments that would return an IterableIterator<number> would have exactly the same type. The * declaration is used by the compiler to allow yield statements but is transparent to the type system.
我们将在后面的章节中回到迭代器和生成器并详细讨论它们。
We will come back to iterators and generators in a later chapter and discuss them at length.
在继续之前,让我们快速回顾一下实现计数器的四种方法和我们学到的各种语言特性:
Before moving on, let’s quickly recap the four ways to implement a counter and the various language features we learned about:
接下来,我们将了解函数类型的另一个常见应用:异步函数。
Next, we’ll look at another common application of function types: asynchronous functions.
实现一个函数,该函数在使用闭包调用时返回斐波那契数列中的下一个数字。
Implement a function that returns the next number in the Fibonacci sequence whenever it is called by using a closure.
实现一个函数,该函数在使用生成器调用时返回斐波那契数列中的下一个数字。
Implement a function that returns the next number in the Fibonacci sequence whenever it is called by using a generator.
我们希望我们的应用程序尽可能快速和响应迅速,即使某些操作需要更长的时间才能完成。按顺序运行我们所有的代码可能会引入不可接受的延迟。如果我们因为等待下载完成而无法响应用户点击按钮,用户会感到沮丧。
We want our applications to be as fast and responsive as possible, even when certain operations take longer to complete. Running all our code sequentially might introduce unacceptable delays. If we can’t respond to our users clicking a button because we’re waiting for a download to complete, the users get frustrated.
通常,我们不想等待一个长时间运行的操作来执行一个更快的操作。最好异步执行此类长时间运行的任务,这样我们就可以在下载完成时保持 UI 交互。异步执行方式这些操作不会按照它们在代码中出现的顺序一个接一个地运行。它们可以并行运行,但这不是强制性的。JavaScript 是单线程的,所以异步执行是通过带有事件循环的运行时来实现的。我们将对使用多线程的并行执行和使用单线程的基于事件循环的执行进行高级描述,但首先,让我们看一个示例,其中异步运行代码会派上用场。
In general, we don’t want to wait for a long-running operation to execute a faster operation. It’s best to execute such long-running tasks asynchronously so we can keep the UI interactive while our download completes. Asynchronous execution means that the operations don’t run one after another, in the order in which they show up in the code. They could be running in parallel, but that’s not mandatory. JavaScript is single-threaded, so asynchronous execution is achieved by the run time with an event loop. We’ll go over a high-level description of both parallel execution using multiple threads and event loop–based execution with a single thread, but first, let’s look at an example in which running code asynchronously comes in handy.
假设我们要执行两个操作:问候我们的用户并将他们带到www.weather.com以便他们可以看到今天的天气。我们将使用两个函数来完成此操作:一个greet()询问用户姓名并向他们打招呼的函数,以及一个weather()启动浏览器了解今天天气的函数。让我们看一下同步实现,然后将其与异步实现进行对比。
Suppose that we want to perform two operations: greet our users and take them to www.weather.com so that they can see today’s weather. We’ll do this with two functions: a greet() function that asks for the user’s name and greets them, and a weather() function, which launches a browser for today’s weather. Let’s look at a synchronous implementation and then contrast it with an asynchronous one.
我们将greet()使用 node 包来实现,如清单 6.9readline-sync所示。这个包提供了一种通过函数读取输入的方法。该函数返回用户键入的字符串。执行阻塞,直到用户键入他们的答案并按下回车键。我们可以使用安装包。 stdinquestion()npm install –save readline-sync
We will implement greet() by using the readline-sync node package, as shown in listing 6.9. This package provides a way to read input from stdin with the question() function. The function returns the string typed by the user. Execution blocks until the user types their answer and presses return. We can install the package with npm install –save readline-sync.
为了实现weather(),我们将使用openNode 包,它允许我们在浏览器中启动 URL。我们可以使用安装包npm install -save open。
To implement weather(), we will use the open Node package, which allows us to launch a URL in the browser. We can install the package with npm install -save open.
函数问候():无效{
const readlineSync = require('readline-sync');
let name: string = readlineSync.question("你叫什么名字?"); 1个
console.log(`嗨 ${name}!`);
}
功能天气():无效{
const open = require('打开');
打开('https://www.weather.com/');
}
迎接(); 2
天气(); 2个function greet(): void {
const readlineSync = require('readline-sync');
let name: string = readlineSync.question("What is your name? "); 1
console.log(`Hi ${name}!`);
}
function weather(): void {
const open = require('open');
open('https://www.weather.com/');
}
greet(); 2
weather(); 2
让我们逐步了解运行此代码时发生的情况。首先,greet()被称为,我们要求用户给我们他们的名字。执行在这里停止,直到我们收到用户的回复,然后它继续输出问候语。返回后greet(),weather()调用,启动www.weather.com。
Let’s step through what happens when we run this code. First, greet() is called, and we ask the user to give us their name. Execution stops here until we receive a reply from the user, after which it proceeds by outputting a greeting. After greet() returns, weather() is called, launching www.weather.com.
此实现有效,但不是最佳的。这两个功能——问候用户和带他们到一个网站——在这种情况下是独立的,因此在另一个完成之前不应阻止其中一个。我们可以以不同的顺序调用这些函数,因为在这种情况下,很明显请求用户输入比启动应用程序花费的时间更长。但在实践中,我们不能总是分辨出两个功能中哪一个需要更长的时间才能完成。更好的方法是异步运行函数。
This implementation works, but it’s not optimal. The two functions—greeting the user and taking them to a website—are independent in this case, so one of them shouldn’t be blocked until the other one finishes. We could call the functions in a different order, because in this case, it’s obvious that requesting user input takes longer than launching an application. But in practice, we can’t always tell which one of two functions will take longer to complete. A better approach is to run the functions asynchronously.
的异步版本会greet()提示用户输入他们的姓名,但不会阻塞并等待回复。执行将通过调用继续weather()。我们仍然想在用户输入姓名后打印出来,所以我们需要一种方法来通知他们的回答。这是通过回调完成的。
An asynchronous version of greet() prompts the user for their name but does not block and wait for the reply. Execution will continue by calling weather(). We still want to print the user’s name after they enter it, so we need a way to be notified of their answer. This is done with a callback.
回调是我们作为参数提供给异步函数的函数。异步函数不会阻塞执行;下一行代码被执行。当长时间运行的操作完成时(在这种情况下,等待用户回答他们的名字),回调函数被执行,所以我们可以处理结果。
A callback is a function that we provide to an asynchronous function as an argument. The asynchronous function does not block execution; the next line of code gets executed. When the long-running operation completes (in this case, waiting for the user to answer with their name), the callback function is executed, so we can handle the result.
greet()让我们看看下一个清单中的异步实现。我们将使用readlineNode.js 提供的模块。在这种情况下,该question()函数不会阻止执行;相反,它将回调作为参数。
Let’s see the asynchronous greet() implementation in the next listing. We will use the readline module provided by Node. In this case, the question() function does not block execution; rather, it takes a callback as an argument.
函数问候():无效{
const readline = require( 'readline' ); 1个
const rl = readline.createInterface({ 2
输入:process.stdin,
输出:process.stdout
});
rl.question("你叫什么名字?", (name: string) => { 3
console.log(`Hi ${name}!`);
rl.close();
} );
}
功能天气():无效{
const open = require('打开');
打开('https://www.weather.com/');
}
迎接();
天气();function greet(): void {
const readline = require('readline'); 1
const rl = readline.createInterface({ 2
input: process.stdin,
output: process.stdout
});
rl.question("What is your name? ", (name: string) => { 3
console.log(`Hi ${name}!`);
rl.close();
});
}
function weather(): void {
const open = require('open');
open('https://www.weather.com/');
}
greet();
weather();
逐步执行此程序,一旦question()调用并提示用户,就会继续执行而不等待用户的回答,从返回greet()并调用weather()。What is your name?运行此程序会在终端上打印“ ”“42”,但www.weather.com将在用户提供答案之前打开。
Stepping through this program, as soon as question() is called and the user is prompted, execution continues without waiting for the user’s answer, returning from greet() and calling weather(). Running this program prints “What is your name?”“42” on the terminal, but www.weather.com will be open before the user provides their answer.
当有答案时,lambda 被调用。lambda 使用 将问候语打印到屏幕上,并console.log()使用 关闭交互式会话(以便不再请求用户输入)rl.close()。
When an answer comes in, the lambda gets called. The lambda prints the greeting to the screen with console.log() and closes the interactive session (so that no more user input is requested) with rl.close().
正如本节开头简要提到的,可以使用线程或事件循环来实现异步执行。选择取决于您的运行时和您使用的库如何实现异步操作。在 JavaScript 中,异步执行是通过事件循环实现的。
As briefly mentioned at the start of this section, asynchronous execution can be achieved with threads or with an event loop. The choice depends on how your run time and the library you are using implement asynchronous operations. In JavaScript, asynchronous execution is implemented with an event loop.
每个应用程序都作为一个进程运行。一个进程从一个主线程开始,但我们可以创建多个其他线程来运行代码。在符合 POSIX 标准的系统(例如 Linux 和 macOS)上,新线程使用 来创建pthread_create(),而 Windows 则提供CreateThread(). 这些 API 由操作系统提供。编程语言为库提供了不同的接口,但这些库最终在内部使用操作系统 API。
Each application runs as a process. A process starts with a main thread, but we can create multiple other threads on which to run code. On POSIX-compliant systems such as Linux and macOS, new threads are created with pthread_create(), whereas Windows provides CreateThread(). These APIs are provided by the operating systems. Programming languages provide libraries with different interfaces, but those libraries end up using the OS APIs internally.
不同的线程可以同时运行。多个 CPU 内核可以并行执行指令,每个处理一个不同的线程。如果线程数大于硬件可以并行运行的数量,操作系统会确保每个线程获得相当多的运行时间。线程调度程序暂停和恢复线程以实现此结果。线程调度器是操作系统内核的核心组件。
Separate threads can run at the same time. Multiple CPU cores can execute instructions in parallel, each handling a different thread. If the number of threads is larger than the hardware can run in parallel, the operating system ensures that each thread gets a fair amount of run time. Threads get paused and resumed by the thread scheduler to achieve this result. The thread scheduler is a core component of the OS kernel.
我们不会查看线程的代码示例,因为 JavaScript(以及 TypeScript)在历史上一直是单线程的。Node 最近启用了对工作线程的实验性支持,但在撰写本文时,这种开发还相当新。也就是说,如果您使用任何其他主流语言进行编程,您可能熟悉如何创建新线程并在其上并行执行代码(图 6.6)。
We won’t look at a code sample for threads, as JavaScript (and, thus, TypeScript) has been historically single-threaded. Node recently enabled experimental support for worker threads, but this development is fairly recent at the time of this writing. That being said, if you program in any other mainstream language, you are probably familiar with how to create new threads and execute code on them in parallel (figure 6.6).
多线程的替代方法是事件循环。事件循环使用队列:异步函数入队,它们自己可以入队其他函数。只要队列不为空,队列中的第一个函数就会出列并执行。
An alternative to multiple threads is an event loop. An event loop uses a queue: asynchronous functions get enqueued, and they themselves can enqueue other functions. As long as the queue is not empty, the first function in line gets dequeued and executed.
作为示例,让我们看一下从给定数字开始倒数的函数,如以下清单所示。该函数不会在倒计时完成之前阻塞执行,而是使用事件队列并将对自身的另一个调用排入队列,直到它达到 0(图 6.7)。
As an example, let’s look at a function that counts down from a given number, shown in the following listing. Instead of blocking execution until the countdown is complete, this function will use an event queue and enqueue another call to itself until it reaches 0 (figure 6.7).
类型 AsyncFunction = () => void; 1个
让队列:AsyncFunction[] = []; 2个
函数 countDown(counterId: string, from: number): void {
console.log(`${counterId}: ${from}`); 3个
如果(从 > 0)
queue.push(() => countDown(counterId, from - 1)); 4个
}
queue.push(() => countDown('counter1', 4)); 5个
while (queue.length > 0) { 6
让 func: AsyncFunction = <AsyncFunction>queue.shift();
功能();
}type AsyncFunction = () => void; 1
let queue: AsyncFunction[] = []; 2
function countDown(counterId: string, from: number): void {
console.log(`${counterId}: ${from}`); 3
if (from > 0)
queue.push(() => countDown(counterId, from - 1)); 4
}
queue.push(() => countDown('counter1', 4)); 5
while (queue.length > 0) { 6
let func: AsyncFunction = <AsyncFunction>queue.shift();
func();
}
此代码将输出
This code will output
柜台1:4 柜台 1: 3 柜台 1: 2 计数器 1:1 计数器 1:0
counter1: 4 counter1: 3 counter1: 2 counter1: 1 counter1: 0
当计数器达到 时0,它不会将另一个呼叫加入队列,因此程序将停止。到目前为止,这并不比简单地在循环中计数更有趣。但是,如果我们首先让两个计数器排队,会发生什么情况?
When the counter reaches 0, it will not enqueue another call, so the program will stop. So far, this isn’t much more interesting than simply counting in a loop. But what happens if we start by enqueuing two counters?
类型 AsyncFunction = () => void;
让队列:AsyncFunction[] = [];
函数 countDown(counterId: string, from: number): void {
console.log(`${counterId}: ${from}`);
如果(从 > 0)
queue.push(() => countDown(counterId, from - 1));
}
queue.push(() => countDown('counter1', 4));
queue.push(() => countDown('counter2', 2)); 1个
while (queue.length > 0) {
让 func: AsyncFunction = <AsyncFunction>queue.shift();
功能();
}type AsyncFunction = () => void;
let queue: AsyncFunction[] = [];
function countDown(counterId: string, from: number): void {
console.log(`${counterId}: ${from}`);
if (from > 0)
queue.push(() => countDown(counterId, from - 1));
}
queue.push(() => countDown('counter1', 4));
queue.push(() => countDown('counter2', 2)); 1
while (queue.length > 0) {
let func: AsyncFunction = <AsyncFunction>queue.shift();
func();
}
这一次,输出是
This time around, the output is
柜台1:4 柜台 2:2 柜台 1: 3 柜台 2: 1 柜台 1: 2 计数器 2:0 计数器 1:1 计数器 1:0
counter1: 4 counter2: 2 counter1: 3 counter2: 1 counter1: 2 counter2: 0 counter1: 1 counter1: 0
正如我们所见,这次计数器是交错的。每个计数器递减一个步骤;然后另一个有机会计数。如果只是循环倒计时,我们是达不到这个结果的。使用队列,这两个函数在倒计时的每一步后产生,并允许其他代码在它们再次倒计时之前运行。
As we can see, this time the counters are interleaved. Each counter counts down one step; then the other one gets a chance to count. We couldn’t achieve this result if we just counted down in a loop. Using the queue, the two functions yield after each step of the countdown and allow other code to run before they count down again.
这两个计数器不会同时运行;要么counter1或者counter2得到一些时间来运行。但它们确实彼此异步或独立运行。他们中的任何一个都可以先完成执行,而不管另一个需要多长时间(图 6.8)。
The two counters do not run at the same time; either counter1 or counter2 gets some time to run. But they do run asynchronously, or independently, of each other. Either of them can finish execution first, regardless of how much longer the other one takes (figure 6.8).
对于等待输入的操作,例如来自键盘的操作,运行时可以确保处理该输入的操作仅在收到输入后才排队,在这种情况下,其他代码可以在提供输入时运行。这样,需要输入的长时间运行的操作可以拆分为两个运行时间较短的操作;第一个请求输入并返回,第二个在输入到达时处理输入。运行时处理在输入可用后安排第二个操作。
For operations that wait for input, such as from the keyboard, the run time can ensure that an operation to handle that input is queued only after input is received, in which case other code can run while the input is being provided. This way, a long--running operation that requires input can be split into two shorter-running ones; the first requests input and returns, and the second processes input when it arrives. The run time handles scheduling the second operation after input is available.
对于无法拆分为多个块的长时间运行的操作,事件循环效果不佳。如果我们排队一个不会产生并运行很长时间的操作,事件循环将被卡住直到它完成。
Event loops don’t work as well for long-running operations that cannot be split into multiple chunks. If we enqueue an operation that doesn’t yield and runs for a long time, the event loop will be stuck until it finishes.
如果我们同步执行长时间运行的操作,则在长时间运行的操作完成之前不会运行其他代码。输入/输出操作是长时间运行操作的好例子,因为从磁盘或网络读取比从内存读取具有更高的延迟。
If we execute long-running operations synchronously, no other code runs until the long-running operation completes. Input/output operations are good examples of long-running operations, as reading from disk or from the network has higher latency than reading from memory.
我们可以异步执行这些操作,而不是同步执行这些操作,并提供一个回调函数,以便在长时间运行的操作完成时调用。执行异步代码有两种主要模型:一种使用多线程,另一种使用事件循环。
Instead of executing such operations synchronously, we can execute them asynchronously and provide a callback function to be called when the long-running operation completes. There are two main models of executing asynchronous code: one that uses multiple threads and one that uses an event loop.
线程可以在单独的 CPU 内核上并行运行,这是它们的主要优势,因为不同的代码片段可以同时运行,并且整个程序可以更快地完成。一个缺点是同步开销:在线程之间传递数据需要小心同步。我们不会在本书中讨论该主题,但您可能听说过诸如死锁和活锁之类的问题,其中两个线程永远不会完成,因为它们相互等待。
Threads can run in parallel on separate CPU cores, which is their main advantage, as different pieces of code can run at the same time, and the overall program finishes faster. A drawback is the synchronization overhead: passing data between threads requires careful synchronization. We won’t cover the topic in this book, but you’ve probably heard of problems such as deadlock and livelock, in which two threads never complete because they wait on each other.
事件循环在单个线程上运行,但启用了一种机制,可以在等待输入时将长时间运行的代码放在队列的后面。使用事件循环的优点是它不需要同步,因为一切都在单个线程上运行。缺点是虽然在等待数据时排队 I/O 操作工作正常,但 CPU 密集型操作仍然阻塞。CPU 密集型操作,如复杂计算,不能只排队;因为它不是在等待数据,所以它需要 CPU 周期。线程更适合这项任务。
An event loop runs on a single thread but enables a mechanism to put long--running code at the back of the queue while it awaits input. The advantage of using an event loop is that it doesn’t require synchronization, as everything runs on a single thread. The disadvantage is that although queuing up I/O operations as they wait for data works fine, CPU-intensive operations still block. A CPU-intensive operation, like a complex computation, can’t just be queued; as it’s not waiting for data, it requires CPU cycles. Threads are much better suited to this task.
大多数主流编程语言都使用线程,JavaScript 是一个明显的例外。话虽这么说,甚至 JavaScript 也在扩展以支持 Web 工作线程(在浏览器中运行的后台线程),并且 Node 对浏览器外的类似工作线程有实验性支持。
Most mainstream programming languages use threads, JavaScript being a notable exception. That being said, even JavaScript is being extended with support for web worker threads (background threads running in the browser), and Node has experimental support for similar worker threads outside the browser.
在下一节中,我们将研究如何使异步代码更清晰、更易于阅读。
In the next section, we look at how we can make our asynchronous code cleaner and easier to read.
以下哪项可用于实现异步执行模型?
- 线程
- 事件循环
- 既不是a也不是b
- a和b都
Which of the following can be used to implement an asynchronous execution model?
- Threads
- An event loop
- Neither a nor b
- Both a and b
在基于事件循环的异步系统中,两个函数可以同时执行吗?
- 是的
- 不
Can two functions execute at the same time in an event-loop-based asynchronous system?
- Yes
- No
在基于线程的异步系统中可以同时执行两个函数吗?
- 是的
- 不
Can two functions execute at the same time in a thread-based asynchronous system?
- Yes
- No
回调的工作方式与前面示例中的计数器相同。计数器在每次运行后将对自身的另一个调用排入队列,而异步函数可以将另一个函数作为参数并在完成执行时将对该函数的调用排入队列。
Callbacks work in the same way as our counter in the preceding example. Whereas the counter enqueues another call to itself after each run, an asynchronous function can take another function as an argument and enqueue a call to that function when it completes execution.
例如,让我们在下一个列表中使用一个在计数器到达后排队的回调来增强我们的计数器0。
As an example, let’s enhance our counter in the next listing with a callback that gets queued after the counter reaches 0.
function countDown(counterId: string, from: number,
callback: () => void ): void { 1
console.log(`${counterId}: ${from}`);
如果(从 > 0)
queue.push(() => countDown(counterId, from – 1, callback));
否则 2
queue.push(回调); 2个
}
queue.push(() => countDown('counter1', 4,
() => console.log('完成') )); 3个function countDown(counterId: string, from: number,
callback: () => void): void { 1
console.log(`${counterId}: ${from}`);
if (from > 0)
queue.push(() => countDown(counterId, from – 1, callback));
else 2
queue.push(callback); 2
}
queue.push(() => countDown('counter1', 4,
() => console.log('Done'))); 3
回调是处理异步代码的常见模式。在我们的示例中,我们使用了不带参数的回调,但回调也可以从异步函数接收参数。question()来自模块的异步调用就是这种情况readline,它将用户提供的字符串传递给回调。
Callbacks are a common pattern for dealing with asynchronous code. In our example, we used a callback without arguments, but callbacks can also receive arguments from the asynchronous function. That was the case with our asynchronous question() call from the readline module, which passed the string provided by the user to the callback.
用回调链接多个异步函数会导致很多嵌套函数,正如我们在代码清单 6.14中看到的,我们想用一个getUserName()函数询问用户姓名,用一个getUser-Birthday()函数询问他们的生日,询问他们的电子邮件地址等等. 这些函数相互依赖,因为它们中的每一个都需要前一个函数的一些信息。(getUser-Birthday()例如,需要用户名。)每个函数也是异步的,因为它可能会长时间运行,所以它需要一个回调来提供其结果。我们使用这些回调来调用链中的下一个函数。
Chaining multiple asynchronous functions with callbacks leads to a lot of nested functions, as we can see in listing 6.14, in which we want to ask the user’s name with a getUserName() function, ask their birthday with a getUser-Birthday() function, ask their email address, and so on. The functions depend on one another because each of them requires some information from the preceding one. (getUser-Birthday() requires the user’s name, for example.) Each function is also asynchronous, as it is potentially long-running, so it takes a callback to provide its result. We use these callbacks to call the next function in the chain.
声明函数 getUserName(
回调:(名称:字符串)=>无效:无效; 1个
声明函数 getUserBirthday(name: string,
回调:(生日:日期)=>无效:无效; 1个
声明函数 getUserEmail(生日: 日期,
回调:(电子邮件:字符串)=>无效:无效; 1个
getUserName((名称: 字符串) => {
console.log(`嗨 ${name}!`);
getUserBirthday(名字, (生日: 日期) => { 2
今天常量:Date = new Date();
如果(生日.getMonth()==今天.getMonth()&&
生日.getDay() == 今天.getDay())
console.log('生日快乐!');
getUserEmail(生日, (email: string) => { 3
/* ... */
});
})
});declare function getUserName(
callback: (name: string) => void): void; 1
declare function getUserBirthday(name: string,
callback: (birthday: Date) => void): void; 1
declare function getUserEmail(birthday: Date,
callback: (email: string) => void): void; 1
getUserName((name: string) => {
console.log(`Hi ${name}!`);
getUserBirthday(name, (birthday: Date) => { 2
const today: Date = new Date();
if (birthday.getMonth() == today.getMonth() &&
birthday.getDay() == today.getDay())
console.log('Happy birthday!');
getUserEmail(birthday, (email: string) => { 3
/* ... */
});
})
});
getUserName()在获取名称时调用的回调中,我们调用getUserBirthday(),将名称传递给它。getUserBirthday()在获取生日时调用的回调中,我们调用getUserEmail()传入生日等。
In the callback invoked when getUserName() obtains the name, we call getUserBirthday(), passing it the name. In the callback invoked when getUserBirthday() obtains the birthday, we call getUserEmail() passing in the birthday and so on.
我们不会在本例中详细介绍所有功能的实际实现,因为它们与上一节中的实现getUser...类似。greet()我们在这里更关心调用代码的整体结构。以这种方式构建代码使其难以阅读,因为我们将更多的回调链接在一起,我们最终在 lambda 中嵌套了更多的 lambda。事实证明,对于这种异步函数调用模式有更好的抽象:promises。
We won’t go over the actual implementation of all the getUser... functions in this example, as they would be similar to the greet()implementation in the preceding section. We’re more concerned here with the overall structure of the calling code. Structuring code this way makes it hard to read, as the more callbacks we chain together, the more nested lambdas inside lambdas we end up with. It turns out that there is a better abstraction for this pattern of asynchronous function calls: promises.
我们首先观察这样一个函数getUserName(callback: (name: string) => void)是一个异步函数,它将在某个时间点确定用户的姓名,然后将其交给我们提供的回调。换句话说,getUserName()“承诺”最终会返回一个名称字符串。我们还观察到,只要函数具有承诺值,我们就希望它调用另一个函数,将该值作为参数传递。
We start by observing that a function such as getUserName(callback: (name: string) => void) is an asynchronous function that will, at some point in time, determine the user’s name and then hand it over to a callback we provide. In other words, getUserName() “promises” to give back a name string eventually. We also observe that whenever the function has the promised value, we want it to call another function, passing that value as an argument.
Promise是在未来某个时间点可用的值的代理。在产生值的代码运行之前,其他代码可以使用 promise 来设置值到达时如何处理、发生错误时如何处理,甚至取消未来的执行。当 promise 的结果可用时设置为调用的函数称为continuation。
A promise is a proxy for a value that will be available at a future point in time. Until the code that produces the value runs, other code can use the promise to set up how the value will be processed when it arrives, what to do in case of error, and even to cancel the future execution. A function set up to be called when the result of a promise is available is called a continuation.
promise 的两个主要成分是T我们的函数“承诺”给我们的某种类型的值,以及将函数指定为T其他类型U( (value: T) => U) 的能力,当 promise 实现并且我们有我们的价值时调用(延续)。这是直接向函数提供回调的替代方法。
The two main ingredients of a promise are a value of some type T that our function “promises” to give us and the ability to specify a function from T to some other type U ((value: T) => U), to be called when the promise is fulfilled and we have our value (a continuation). This is an alternative to supplying the callback directly to a function.
首先,让我们更新清单 6.15中函数的声明,这样它们就不会采用回调参数,而是返回一个Promise. getUserName()将返回一个Promise-<string>,getUserBirthday()将返回一个Promise<Date>,并将getUser-Email()返回另一个Promise<string>。
First, let’s update the declarations of our functions in listing 6.15 so that instead of taking a callback argument, they return a Promise. getUserName() will return a Promise-<string>, getUserBirthday() will return a Promise<Date>, and getUser-Email() will return another Promise<string>.
声明函数 getUserName(): Promise<string>; 声明函数 getUserBirthday(name: string): Promise<Date>; 声明函数 getUserEmail(birthday: Date): Promise<string>;
declare function getUserName(): Promise<string>; declare function getUserBirthday(name: string): Promise<Date>; declare function getUserEmail(birthday: Date): Promise<string>;
JavaScript(以及 TypeScript)提供了一个内置Promise<T>类型来实现这种抽象。在 C# 中,Task<T>实现了这一点,在 Java 中,CompletableFuture<T>提供了类似的功能。
JavaScript (and, thus, TypeScript) provides a built-in Promise<T> type that implements this abstraction. In C#, Task<T> implements this, and in Java, CompletableFuture<T> provides similar functionality.
promise 提供了一种then()允许我们传递延续的方法。每个then()函数返回另一个承诺,因此我们可以将调用then()链接在一起。这个过程消除了我们在基于回调的实现中看到的嵌套。
A promise provides a then() method that allows us to pass in our continuation. Each then() function returns another promise, so we can chain then() calls together. This process eliminates the nesting we saw in the callback-based implementation.
获取用户名()
.then((名称: 字符串) => { 1
console.log(`嗨 ${name}!`);
返回 getUserBirthday(名字); 2个
})
.then((生日: 日期) => { 3
今天常量:Date = new Date();
如果(生日.getMonth()==今天.getMonth()&&
生日.getDay() == 今天.getDay())
console.log('生日快乐!');
返回 getUserEmail(生日);
})
.then((email: string) => { 4
/* ... */
});getUserName()
.then((name: string) => { 1
console.log(`Hi ${name}!`);
return getUserBirthday(name); 2
})
.then((birthday: Date) => { 3
const today: Date = new Date();
if (birthday.getMonth() == today.getMonth() &&
birthday.getDay() == today.getDay())
console.log('Happy birthday!');
return getUserEmail(birthday);
})
.then((email: string) => { 4
/* ... */
});
正如我们所看到的,不是在回调中的回调中有回调,而是以一种更容易遵循的模式将延续链接在一起:我们运行一个函数,then()我们运行另一个函数,等等。
As we can see, instead of having a callback within a callback within a callback, continuations are chained together in a pattern that’s easier to follow: we run a function, then() we run another function, and so on.
如果我们想使用这个模式,我们还应该看看如何创建一个承诺。原理很简单,尽管它依赖于高阶函数——一个 promise 将一个函数作为参数,该函数将另一个函数作为参数——所以乍一看似乎令人费解。
If we want to use this pattern, we should also look at how we can create a promise. The principle is straightforward, though it relies on higher-order functions—a promise takes as argument a function that takes as argument another function—so it may seem mind-bending at first.
对某种类型的值(例如 )的承诺Promise<string>并不知道如何计算该值。它then()为我们之前看到的continuation chaining 提供了一种方法,但是它不能确定字符串是什么。在 的情况下getUserName(),承诺的字符串是用户的姓名,在 的情况下getUserEmail(),承诺的字符串是电子邮件地址。那么,仿制药如何Promise<string>能够确定该值呢?答案是它不能没有帮助。promise 的构造函数将实际处理值计算的函数作为参数。对于getUserName(),该功能会提示用户输入他们的姓名并得到他们的回复。然后,promise 可以通过直接调用它来使用这个函数,将它排入事件循环队列,或者安排它的执行在一个线程上,取决于实现,它因语言和库而异。
A promise for a value of a certain type, such as Promise<string>, doesn’t really know how to compute that value. It provides a then() method for the continuation chaining we saw before, but it cannot determine what the string is. In the case of getUserName(), the promised string is the name of the user, and in the case of getUserEmail(), the promised string is an email address. How, then, could a generic Promise<string> be able to determine that value? The answer is that it can’t without help. The constructor of a promise takes as an argument a function that actually handles computing the value. For getUserName(), that function would prompt the user for their name and get their reply. The promise can then use this function by calling it directly, queuing it for the event loop, or scheduling its execution on a thread, depending on the implementation, which differs from language to language and library to library.
到目前为止,一切都很好。获取Promise<string>一些将提供值的代码。但是因为该代码可能会在稍后运行,我们还需要一种机制让该代码告诉 promise 值已经到达。对于该任务,promise 将传递一个调用resolve()该代码的函数。确定值后,代码可以调用resolve()并将值交还给承诺(图 6.9)。
So far, so good. The Promise<string> gets some code that will provide the value. But because that code might run at a later time, we also need a mechanism for that code to tell the promise that the value has arrived. For that task, the promise will pass a function called resolve() to that code. When the value is determined, the code can call resolve() and hand the value back to the promise (figure 6.9).
让我们看看我们如何getUserName()在下一个清单中实现返回一个承诺。
Let’s look at how we can implement getUserName() in the next listing to return a promise.
函数 getUserName(): Promise<string> {
返回新的 Promise<string>(
(解析:(值:字符串)=> void)=> { 1
const readline = require('readline'); 2个
const rl = readline.createInterface({ 2
输入:process.stdin,
输出:process.stdout
});
rl.question("你叫什么名字?", (name: string) => { 2
rl.close();
解决(名称); 3个
});
});
}function getUserName(): Promise<string> {
return new Promise<string>(
(resolve: (value: string) => void) => { 1
const readline = require('readline'); 2
const rl = readline.createInterface({ 2
input: process.stdin,
output: process.stdout
});
rl.question("What is your name? ", (name: string) => { 2
rl.close();
resolve(name); 3
});
});
}
getUserName()简单地创建并返回一个承诺。promise 是用一个带有resolvetype 参数的函数初始化的(value: string) => void。此函数包含要求用户提供其姓名的代码,提供姓名后,该函数将调用 resolve()以将值传递给承诺。
getUserName() simply creates and returns a promise. The promise is initialized with a function that takes a resolve argument of type (value: string) => void. This function contains the code to ask the user to provide their name, and when the name is provided, the function calls resolve() to pass the value to the promise.
如果我们实现长时间运行的函数来返回承诺,我们可以通过使用将这些异步调用链接在一起,Promise.then()使我们的代码更具可读性。
If we implement long-running functions to return promises, we can chain these asynchronous calls together by using Promise.then() to make our code more readable.
承诺不仅仅是提供延续。让我们看看 promise 如何处理错误,以及除了使用then().
There’s more to promises than providing continuations. Let’s see how promises handle errors and a couple more ways to sequence their execution beyond using then().
承诺可以处于三种状态之一:未决、已解决和已拒绝。Pending表示 promise 已创建但尚未解决(即负责提供值的 provided 函数尚未调用resolve())。已解决意味着resolve()已调用并提供了一个值,此时调用了延续。但是如果出现错误怎么办?当负责提供值的函数抛出异常时,promise 进入拒绝状态。
A promise can be in one of three states: pending, settled, and rejected. Pending means that the promise has been created but not yet resolved (that is, the provided function responsible for providing a value hasn’t called resolve() yet). Settled means that resolve() was called and a value is provided, at which point continuations are called. But what happens if there is an error? When the function responsible for providing a value throws an exception, the promise enters the rejected state.
事实上,负责为 promise 提供值的函数可以将附加函数作为参数,因此它可以将 promise 设置为拒绝状态并提供拒绝的原因。而不是提供
In fact, the function responsible for providing a value to the promise can take an additional function as an argument, so it can set the promise in the rejected state and provide a reason for that. Instead of providing
(解决:(值:T)=>无效)=>无效
(resolve: (value: T) => void) => void
对于构造函数,调用者可以提供一个
to the constructor, callers can provide a
(解决:(值:T)=>无效,拒绝:(原因:任何)=>无效)=>无效
(resolve: (value: T) => void, reject: (reason: any) => void) => void
第二个参数是一个函数(reason: any) => void,它可以为承诺提供任何类型的原因并将其标记为已拒绝。
The second argument is a function (reason: any) => void, which can provide a reason of any type to the promise and mark it as rejected.
即使不调用reject(),如果函数抛出异常,promise 也会自动认为自己被拒绝。除了then()函数之外,promise 还公开了一个catch()函数,我们可以在其中提供一个延续,当 promise 因任何原因被拒绝时被调用(图 6.10)。
Even without calling reject(), if the function throws an exception, the promise will automatically consider itself to be rejected. Besides the then() function, a promise exposes a catch() function in which we can provide a continuation to be called when the promise is rejected for whatever reason (figure 6.10).
让我们扩展我们的getUserName()函数以拒绝下一个列表中的空字符串。
Let’s extend our getUserName() function to reject an empty string in the next listing.
函数 getUserName(): Promise<string> {
const readline = require('readline');
const rl = readline.createInterface({
输入:process.stdin,
输出:process.stdout
});
返回新的 Promise<string>(
(解析:(值:字符串)=>无效,
拒绝:(原因:字符串)=>无效)=> { 1
rl.question("你叫什么名字?", (name: string) => {
rl.close();
如果(名称。长度!= 0){
解决(名称);
} 别的 {
reject("姓名不能为空"); 2个
}
});
});
}
获取用户名()
.then((name: string) => { console.log(`Hi ${name}!`); })
.catch((reason: string) => { console.log(`Error: ${reason}`); }); 3个function getUserName(): Promise<string> {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise<string>(
(resolve: (value: string) => void,
reject: (reason: string) => void) => { 1
rl.question("What is your name? ", (name: string) => {
rl.close();
if (name.length != 0) {
resolve(name);
} else {
reject("Name can't be empty"); 2
}
});
});
}
getUserName()
.then((name: string) => { console.log(`Hi ${name}!`); })
.catch((reason: string) => { console.log(`Error: ${reason}`); }); 3
一个 promise 不仅会通过调用reject()或由于抛出错误而被拒绝,而且通过then()get rejected 链接到它的所有其他 promise 也会被拒绝。如果链中的任何承诺被拒绝,将调用在调用链 catch()末尾添加的延续。then()
Not only does a promise get rejected, either via a call to reject() or due to an error being thrown, but also all other promises chained to it via then() get rejected. A catch() continuation added at the end of a chain of then() calls will get called if any of the promises in the chain is rejected.
将延续链接在一起的方法比我们目前所介绍的要多。首先,continuation 不必返回 promise。我们并不总是链接异步函数;也许延续是短期运行的,可以同步执行。让我们再看看下面清单中的原始示例,其中我们所有的延续都返回了承诺。
There are more ways to chain continuations together than what we’ve covered so far. First, a continuation doesn’t have to return a promise. We don’t always chain asynchronous functions; maybe the continuation is short-running and can be executed synchronously. Let’s take another look at our original example in the following listing, in which all our continuations returned promises.
获取用户名() 1
.then((名称: 字符串) => {
console.log(`嗨 ${name}!`);
返回 getUserBirthday(名字); 2个
})
.then((生日: 日期) => {
今天常量:Date = new Date();
如果(生日.getMonth()==今天.getMonth()&&
生日.getDay() == 今天.getDay())
console.log('生日快乐!');
返回 getUserEmail(生日); 3个
})
.then((email: string) => {
/* ... */
});getUserName() 1
.then((name: string) => {
console.log(`Hi ${name}!`);
return getUserBirthday(name); 2
})
.then((birthday: Date) => {
const today: Date = new Date();
if (birthday.getMonth() == today.getMonth() &&
birthday.getDay() == today.getDay())
console.log('Happy birthday!');
return getUserEmail(birthday); 3
})
.then((email: string) => {
/* ... */
});
在这种情况下,我们所有的函数都需要异步运行,因为它们需要用户输入。但是,如果在我们获得用户名后,我们只是想将它拼接在一个字符串中并返回它呢?如果我们的延续只是return `Hi ${name}!`,它返回一个字符串,而不是一个承诺。但没关系;该then()函数会自动将其转换为 a Promise-<string>,以便可以由另一个延续进一步处理,如以下代码所示。
In this case, all our functions need to run asynchronously, as they expect user input. But what if after we get the user’s name, we simply want to splice it inside a string and return that? If our continuation is just return `Hi ${name}!`, it returns a string, not a promise. But that’s OK; the then() function automatically converts it in a Promise-<string> so that it can be further processed by another continuation, as shown in the following code.
获取用户名()
.then((名称: 字符串) => {
返回`嗨${name}!`; 1个
})
.then((问候语:字符串) => {
控制台日志(问候语);
});getUserName()
.then((name: string) => {
return `Hi ${name}!`; 1
})
.then((greeting: string) => {
console.log(greeting);
});
这在直觉上应该是有意义的:即使我们的延续只是返回一个字符串,因为它被链接到一个承诺,它不能立即执行。这一事实自动使它在最初的承诺得到解决时成为一个承诺。
This should make sense intuitively: even if our continuation just returns a string, because it is chained to a promise, it can’t execute right away. That fact automatically makes it a promise to be settled when the original promise is settled.
到目前为止,我们已经研究了then()(and catch()),哪个链式承诺在一起,以便它们一个接一个地结算。还有更多方法可以安排异步函数的执行:通过Promise.all()和Promise.race()。这些是类上提供的静态方法Promise。Promise.all()将一组承诺作为参数,并返回一个承诺,该承诺在所有提供的承诺都已结算时结算。Promise.race()接受一组承诺并返回一个承诺,该承诺在任何一个承诺得到解决时得到解决。
So far, we’ve looked at then() (and catch()), which chain promises together so that they settle one after the other. There are a couple more ways to schedule the execution of asynchronous functions: via Promise.all() and Promise.race(). These are static methods provided on the Promise class. Promise.all() takes as arguments a set of promises and returns a promise that is settled when all the provided promises are settled. Promise.race() takes a set of promises and returns a promise that is settled when any one of the promises is settled.
当我们想要调度一组独立的异步函数时,我们可以使用Promise.all(),例如从数据库中获取用户收件箱消息和从 CDN 中获取他们的个人资料图片,然后将这两个值传递给 UI,如清单 6.21所示。我们不想一个接一个地对这些获取函数进行排序,因为它们并不相互依赖。另一方面,我们确实想收集它们的结果并将它们传递给另一个函数。
We can use Promise.all() when we want to schedule a set of independent asynchronous functions, such as fetching user inbox messages from a database and their profile picture from a CDN, and then passing both values to the UI, as shown in listing 6.21. We don’t want to sequence these fetching functions one after another, because they don’t depend on one another. On the other hand, we do want to gather their results and pass them to another function.
类 InboxMessage { /* ... */ }
类 ProfilePicture { /* ... */ }
声明函数 getInboxMessages(): Promise<InboxMessage[]>; 1
声明函数 getProfilePicture(): Promise<ProfilePicture>; 1个
声明函数 renderUI( 2
消息:InboxMessage[],图片:ProfilePicture:无效;
Promise.all([getInboxMessages(), getProfilePicture()]) 3
.then((values: [InboxMessage[], ProfilePicture]) => { 4
renderUI(values[0], values[1]); 5
});class InboxMessage { /* ... */ }
class ProfilePicture { /* ... */ }
declare function getInboxMessages(): Promise<InboxMessage[]>; 1
declare function getProfilePicture(): Promise<ProfilePicture>; 1
declare function renderUI( 2
messages: InboxMessage[], picture: ProfilePicture): void;
Promise.all([getInboxMessages(), getProfilePicture()]) 3
.then((values: [InboxMessage[], ProfilePicture]) => { 4
renderUI(values[0], values[1]); 5
});
像这样的模式将很难通过回调实现,因为没有加入回调的机制。
A pattern like this would be significantly harder to achieve with callbacks, as there is no mechanism to join them.
Promise.race()让我们看一下在下一个清单中使用的示例。假设跨两个节点复制用户配置文件。我们尝试从两者中获取它,以最快的为准。在这种情况下,只要我们从任何一个节点获得结果,我们就可以继续。
Let’s look at an example of using Promise.race() in the next listing. Suppose that the user profile is replicated across two nodes. We try to fetch it from both, and whichever is the fastest wins. In this case, as soon as we get a result from any one of the nodes, we can proceed.
类 UserProfile { /* ... */ }
声明函数 getProfile(node: string): Promise<UserProfile>;
声明函数 renderUI(profile: UserProfile): void;
Promise.race([getProfile("node1"), getProfile("node2")]) 1
.then((profile: UserProfile) => { 2
renderUI(配置文件);
});class UserProfile { /* ... */ }
declare function getProfile(node: string): Promise<UserProfile>;
declare function renderUI(profile: UserProfile): void;
Promise.race([getProfile("node1"), getProfile("node2")]) 1
.then((profile: UserProfile) => { 2
renderUI(profile);
});
如果使用没有承诺的回调,这种情况将更难实现(图 6.11)。
This scenario would be more difficult to achieve by using callbacks without promises (figure 6.11).
Promises 为运行异步函数提供了一个清晰的抽象。它们不仅使代码比通过then()andcatch()方法使用回调更具可读性,这可以实现排序,而且还可以处理错误传播以及通过Promise.all()and加入或竞争多个承诺Promise.race()加入或竞争多个承诺。大多数主流编程语言都提供 Promise 库,它们都提供类似的功能,即使方法的名称略有不同。(race()有时称为any(),例如。)
Promises provide a clean abstraction for running asynchronous functions. They not only make code more readable than using callbacks through the then() and catch() methods, which enable sequencing, but also handle error propagation and joining or racing multiple promises via Promise.all() and Promise.race(). Promise libraries are available in most mainstream programming languages, and they all provide similar functionality, even if the name of the methods is slightly different. (race() is sometimes called any(), for example.)
这是库在帮助我们编写干净的异步代码方面所能达到的极限。使异步代码更具可读性需要更新语言本身的语法。就像yield语句让我们更轻松地表达生成器函数一样,许多语言扩展了它们的语法async,await使我们能够更轻松地编写异步函数。
This is about as far as libraries can go in helping us write clean asynchronous code. Making asynchronous code more readable requires updates to the syntax of the language itself. Much as a yield statement allows us to more easily express a generator function, many languages extended their syntax with async and await to enable us to write asynchronous functions more easily.
使用 promises,我们提示我们的用户提供各种信息,使用 continuations 对问题进行排序。让我们再看看下一个清单中的实现。我们要把它包装成一个getUserData()函数。
Using promises, we prompted our user for various pieces of information, using continuations to sequence the questions. Let’s take another look at that implementation in the next listing. We’re going to wrap it into a getUserData() function.
函数 getUserData(): void {
获取用户名()
.then((名称: 字符串) => {
console.log(`嗨 ${name}!`);
返回 getUserBirthday(名字);
})
.then((生日: 日期) => {
今天常量:Date = new Date();
如果(生日.getMonth()==今天.getMonth()&&
生日.getDay() == 今天.getDay())
console.log('生日快乐!');
返回 getUserEmail(生日);
})
.then((email: string) => {
/* ... */
});
}function getUserData(): void {
getUserName()
.then((name: string) => {
console.log(`Hi ${name}!`);
return getUserBirthday(name);
})
.then((birthday: Date) => {
const today: Date = new Date();
if (birthday.getMonth() == today.getMonth() &&
birthday.getDay() == today.getDay())
console.log('Happy birthday!');
return getUserEmail(birthday);
})
.then((email: string) => {
/* ... */
});
}
再次注意,每个延续都将一个值作为参数,该值的类型与前一个函数的承诺类型相同。async/await允许我们在代码中更好地表达这一点。我们可以将生成器和*/yield我们在上一节中讨论的语法进行比较。
Notice again that each continuation takes as argument a value of the same type as the type of the promise from the preceding function. async/await allows us to express this better in code. We can draw a parallel with generators and the */yield syntax we discussed in a previous section.
async是出现在关键字之前的关键字function,就像在生成器中*出现在关键字之后一样function。与*can only used only if the function returns an相同Iterator,async只能出现在返回 a 的函数中Promise,就像*,async不会改变函数的类型一样。function getUserData(): Promise<string>并且async function getUserData(): Promise<string>具有相同的类型:() => Promise<string>. *将函数标记为生成器并允许我们yield在其中调用的方式与async将函数标记为异步函数并允许我们await在其中 调用的方式相同。
async is a keyword that comes before the keyword function, much as the * appears after the keyword function in generators. In the same way that * can be used only if the function returns an Iterator, async can appear only in a function that returns a Promise, just as *, async does not change the type of the function. function getUserData(): Promise<string> and async function getUserData(): Promise<string> have the same type: () => Promise<string>. The same way that * marks a function as a generator and allows us to call yield inside it, async marks a function as asynchronous and allows us to call await inside it.
我们可以await在返回承诺的函数之前使用,以获取该承诺结算时返回的值。我们不写作getUserName().then -((name: string) => { /* ... */ }),而是写作let name: string = await getUser-Name()。在了解它是如何工作的之前,让我们看看我们将如何getUserData()使用asyncand编写await。
We can use await before a function that returns a promise to get the value returned when that promise settles. Instead of writing getUserName().then -((name: string) => { /* ... */ }), we write let name: string = await getUser-Name(). Before walking through how this works, let’s look at how we would write getUserData() with async and await.
async function getUserData(): Promise<void> { 1
let name: string = await getUserName(); 2
console.log(`嗨 ${name}!`); 3个
让生日:Date = await getUserBirthday(name); 4个
今天常量:Date = new Date();
如果(生日.getMonth()==今天.getMonth()&&
生日.getDay() == 今天.getDay())
console.log('生日快乐!');
let email: string = await getUserEmail(birthday); 5个
/* ... */
}async function getUserData(): Promise<void> { 1
let name: string = await getUserName(); 2
console.log(`Hi ${name}!`); 3
let birthday: Date = await getUserBirthday(name); 4
const today: Date = new Date();
if (birthday.getMonth() == today.getMonth() &&
birthday.getDay() == today.getDay())
console.log('Happy birthday!');
let email: string = await getUserEmail(birthday); 5
/* ... */
}
我们立即看到,getUserData()以这种方式编写我们的代码比将 promise 与then(). 编译器生成相同的代码;引擎盖下没有什么特别的。这种技术只是一种表达延续链的更好方式。then()我们可以将所有代码写在一个函数中,而不是将每个延续放在一个单独的函数中并通过 连接它们,每当我们调用另一个返回承诺的函数时,我们await就会返回它的结果。
We immediately see that writing our getUserData() this way makes it even more readable than chaining promises with then(). The compiler generates the same code; there is nothing special under the hood. This technique is simply a nicer way to express a chain of continuations. Instead of putting each continuation in a separate function and connecting them via then(), we can write all the code in a single function, and whenever we call another function that returns a promise, we await its result.
每个都await相当于在它之后获取代码并将其放在一个then()延续中:这减少了我们需要编写的 lambda 的数量,并使异步代码像同步代码一样读取。至于catch(),如果没有返回值,可能是我们遇到了异常,异常从调用中抛出,可以用正则/语句await捕获。只需将调用包装在一个块中即可捕获预期的错误。 trycatchawaittry
Each await is the equivalent of taking the code after it and placing it in a then() continuation: this reduces the number of lambdas we need to write and makes asynchronous code read just like synchronous code. As for catch(), if there is no value to return, perhaps because we encountered an exception, the exception is thrown from the await call and can be caught with a regular try/catch statement. Simply wrap the await call in a try block to catch the expected errors.
让我们快速回顾一下本节介绍的编写异步代码的方法。我们从回调开始,将回调函数传递给一个异步函数,该函数在其工作完成时调用它。这种方法可行,但我们通常会在回调中出现大量嵌套回调,这使得代码更难理解。如果我们需要所有异步函数的结果才能继续,那么加入几个独立的异步函数也是非常困难的。
Let’s quickly review the approaches to writing asynchronous code that we covered in this section. We started with callbacks, passing a callback function to an asynchronous function that calls it when its work is done. This approach works, but we’ll usually end up with a lot of nested callbacks within callbacks, which makes code harder to follow. It’s also very difficult to join several independent asynchronous functions if we need the results from all of them to proceed.
接下来,我们看看承诺。Promises 为编写异步代码提供了抽象。它们处理代码执行的调度(在依赖线程的语言中,它们是在线程上调度的)并为我们提供了一种方法来提供称为延续的函数,当承诺被解决(具有价值)或被拒绝(遇到错误)。Promise.all()Promises 还提供了通过和加入和竞争一组 promise 的方法Promise.race()。
Next, we looked at promises. Promises provide an abstraction for writing asynchronous code. They handle scheduling the execution of the code (in languages that rely on threads, they get scheduled on threads) and provide a way for us to provide functions called continuations, which get called when the promise is settled (has a value) or rejected (encountered an error). Promises also provide ways to join and race a set of promises via Promise.all() and Promise.race().
最后,async/await语法现在在大多数主流编程语言中都很常见,它提供了一种更简洁的方式来编写读取起来就像常规代码一样的异步代码。then()我们不是提供一个延续,而是await一个承诺的结果,并从那里继续。计算机执行的底层代码是相同的,但语法更易读。
Finally, async/await syntax, now common in most mainstream programming languages, provides an even-cleaner way to write asynchronous code that reads just like regular code. Instead of providing a continuation with then(), we await the result of a promise and continue from there. The underlying code executed by the computer is the same, but the syntax is much nicer to read.
承诺从哪个状态开始?
- 入驻
- 拒绝
- 待办的
- 任何
Which state does a promise start in?
- Settled
- Rejected
- Pending
- Any
当 promise 被拒绝时,以下哪个链将被调用?
- then()
- catch()
- all()
- race()
Which of the following chains a continuation to be called when the promise is rejected?
- then()
- catch()
- all()
- race()
当一整套 promises 被解决时,以下哪个链将被调用?
- then()
- catch()
- all()
- race()
Which of the following chains a continuation to be called when a whole set of promises is settled?
- then()
- catch()
- all()
- race()
现在我们已经深入介绍了函数类型的应用,从将函数作为参数传递到生成器和异步函数的基础知识,我们将继续讨论下一个主要主题:子类型。正如我们将在第 7 章中看到的,子类型比继承要多得多。
Now that we’ve covered applications of function types in depth, from the basics of passing functions as arguments all the way to generators and asynchronous functions, we’ll move on to the next major topic: subtypes. As we’ll see in chapter 7, there is a lot more to subtypes than inheritance.
一个可能的实现返回一个将日志记录添加到包装工厂的函数:
函数 loggingDecorator(工厂:()=> 小部件):()=> 小部件 { 返回()=> { console.log("Widget 已创建"); 返回工厂(); } }
A possible implementation returning a function that adds logging to the wrapped factory:
function loggingDecorator(factory: () => Widget): () => Widget { return () => { console.log("Widget created"); return factory(); } }
使用从包装函数 捕获a和捕获的闭包的可能实现:b
函数 fib(): () => 数字 { 让一个:数字= 0; 让 b: 数字 = 1; 返回()=> { 让下一个:数字=一个; 一 = b; b = b + 下一个; 接下来返回; } }
A possible implementation using a closure that captures a and b from the wrapping function:
function fib(): () => number { let a: number = 0; let b: number = 1; return () => { let next: number = a; a = b; b = b + next; return next; } }
使用生成序列中下一个数字的生成器的可能实现:
函数 *fib2(): IterableIterator<number> { 让一个:数字= 0; 让 b: 数字 = 1; 而(真){ 让下一个:数字=一个; 一 = b; b = a + 下一个; 接下来屈服; } }
A possible implementation using a generator that yields the next number in the sequence:
function *fib2(): IterableIterator<number> { let a: number = 0; let b: number = 1; while (true) { let next: number = a; a = b; b = a + next; yield next; } }
d—线程和事件循环都可以用来实现异步执行。
d—Both threads and an event loop can be used to implement asynchronous execution.
b—事件循环不并行执行代码。它可以异步排队和执行功能,但不能同时进行。
b—An event loop does not execute code in parallel. It can queue and execute functions asynchronously, but not at the same time.
a——线程允许并行执行;多个线程可以同时运行多个函数。
a—Threads allow parallel execution; multiple threads can run multiple functions at the same time.
c—一个 promise 从 pending 状态开始。
c—A promise starts in the pending state.
c—我们用来catch()链接一个在承诺被拒绝时被调用的延续。
c—We use catch() to chain a continuation that gets called when a promise is rejected.
c—我们用来all()链接一个延续,当所有的承诺都被解决时,它会被调用。
c—We use all() to chain a continuation that gets called when all promises are settled.
本章涵盖
This chapter covers
现在我们已经介绍了基本类型、组合和函数类型,是时候看看类型系统的另一个方面了:类型之间的关系。在本章中,我们将介绍子类型关系。尽管您可能从面向对象编程中熟悉它,但我们不会在本章中介绍继承。相反,我们将专注于一组不同的子类型应用。
Now that we’ve covered primitive types, composition, and function types, it’s time to look at another aspect of type systems: relationships between types. In this chapter, we’ll introduce the subtyping relationship. Although you may be familiar with it from object-oriented programming, we will not cover inheritance in this chapter. Instead, we will focus on a different set of applications of subtyping.
首先,我们讨论什么是子类型化以及编程语言实现它的两种方式:结构化和名义化。然后我们将重新审视我们的火星气候轨道器示例,并解释unique symbol我们在第 4 章讨论类型安全时使用的技巧。
First, we’ll talk about what subtyping is and the two ways in which programming languages implement it: structural and nominal. Then we will revisit our Mars Climate Orbiter example and explain the unique symbol trick we used in chapter 4 when discussing type safety.
因为一个类型可以是另一个类型的子类型,它也可以有其他子类型,我们将看看这个类型层次结构:我们通常有一个类型位于这个层次结构的顶部,有时,一个类型位于这个层次结构的顶部底部。我们将看到如何在诸如反序列化之类的场景中使用这种顶级类型,在这种场景中我们没有大量可用的类型信息。我们还将看到如何使用底部类型作为错误情况的值。
Because a type can be a subtype of another type, and it can also have other subtypes, we will look at this type hierarchy: we usually have a type that sits at the top of this hierarchy and, sometimes, a type that sits at the bottom. We’ll see how we can use this top type in a scenario such as deserialization, in which we don’t have a lot of typing information readily available. We’ll also see how to use a bottom type as a value for error cases.
在本章的后半部分,我们将研究更复杂的子类型关系是如何建立的。这有助于我们了解哪些价值观可以替代哪些其他价值观。我们是否需要实现包装器,或者我们可以简单地按原样传递另一种类型的值吗?如果一种类型是另一种类型的子类型,那么这两种类型的集合之间的子类型关系是什么?接受或返回这些类型的参数的函数呢?我们将举一个涉及形状的简单示例,看看我们如何将它们作为求和类型、集合和函数传递,这个过程也称为方差。我们还将了解不同类型的方差。但首先,让我们看看子类型在 TypeScript 中的含义。
In the second half of the chapter, we will look at how more-complex subtyping relationships are established. This helps us understand what values we can substitute for what other values. Do we need to implement wrappers, or can we simply pass a value of another type as is? If a type is a subtype of another type, what is the subtyping relationship between collections of those two types? What about functions that take or return arguments of these types? We’ll take a simple example involving shapes and see how we can pass them around as sum types, collections, and functions, a process also known as variance. We’ll also learn about the different types of variance. But first, let’s see what subtyping means in TypeScript.
本书中的大多数示例,即使是用 TypeScript 呈现的,也是与语言无关的,可以翻译成大多数其他主流编程语言。本节例外;我们将讨论一种特定于 TypeScript 的技术。我们这样做是因为它是讨论子类型的一个很好的转折点。
Most of the examples in this book, even though presented in TypeScript, are language-agnostic and can be translated for most other mainstream programming languages. This section is an exception; we’ll discuss a technique specific to TypeScript. We’ll do this because it’s a great segue into a discussion of subtyping.
让我们重新审视第 4 章中的磅力秒/牛顿秒示例。请记住,我们将两个不同的测量单位建模为两个不同的类别。我们想确保类型检查器不允许我们将一种类型的值误解为另一种类型,因此我们过去常常unique symbol消除它们的歧义。我们没有详细说明为什么我们当时必须这样做,但现在让我们在下面的清单中这样做。
Let’s revisit the pound-force second/Newton-second example from chapter 4. Remember that we were modeling two different units of measurements as two different classes. We wanted to make sure that the type checker wouldn’t allow us to misinterpret a value of one type as the other, so we used unique symbol to disambiguate them. We didn’t go into the details of why we had to do this then, but let’s do it now in the following listing.
声明 const NsType:唯一符号; 1个
类 Ns {
值:数字;
[NsType] : void; 1个
构造函数(值:数字){
this.value = 值;
}
}
声明 const LbfsType:唯一符号; 2个
类 Lbfs {
值:数字;
[LbfsType] : void; 2个
构造函数(值:数字){
this.value = 值;
}
}declare const NsType: unique symbol; 1
class Ns {
value: number;
[NsType]: void; 1
constructor(value: number) {
this.value = value;
}
}
declare const LbfsType: unique symbol; 2
class Lbfs {
value: number;
[LbfsType]: void; 2
constructor(value: number) {
this.value = value;
}
}
如果我们省略这两个声明,就会发生一件有趣的事情:我们可以将一个Ns对象作为一个Lbfs对象传递,反之亦然,而不会从编译器中得到任何错误。让我们实现一个函数来演示这个过程:一个名为的函数acceptNs()需要一个Ns参数。然后我们将尝试在下一个清单 Lbfs中将一个对象传递给。acceptNs()
If we omit these two declarations, an interesting thing happens: we can pass a Ns object as a Lbfs object, and vice versa, without getting any errors from the compiler. Let’s implement a function to demonstrate this process: a function named acceptNs() that expects a Ns argument. Then we’ll try to pass a Lbfs object to acceptNs() in the next listing.
类 Ns { 1
值:数字;
构造函数(值:数字){
this.value = 值;
}
}
Lbfs 类 { 1
值:数字;
构造函数(值:数字){
this.value = 值;
}
}
函数 acceptNs(momentum: Ns): void { 2
console.log(`Momentum: ${momentum.value} Ns`);
}
acceptNs(新 Lbfs(10)); 3个class Ns { 1
value: number;
constructor(value: number) {
this.value = value;
}
}
class Lbfs { 1
value: number;
constructor(value: number) {
this.value = value;
}
}
function acceptNs(momentum: Ns): void { 2
console.log(`Momentum: ${momentum.value} Ns`);
}
acceptNs(new Lbfs(10)); 3
令人惊讶的是,这段代码可以运行并记录Momentum: 10 Ns.,这绝对不是我们想要的。我们定义这两种不同类型的原因是为了避免混淆这两种测量单位和使火星气候轨道器坠毁。这是怎么回事?要了解发生了什么,我们需要了解子类型。
Surprisingly, this code works and logs Momentum: 10 Ns., which is definitely not what we want. The reason why we defined these two separate types was to avoid confusing the two units of measure and crashing the Mars Climate Orbiter. What’s going on? To understand what is happening, we need to understand subtyping.
如果一个类型的实例可以安全地用于任何需要 一个实例的地方,那么这个类型S就是一个类型的子类型。TST
A type S is a subtype of a type T if an instance of S can be safely used anywhere an instance of T is expected.
这是著名的Liskov 替换原则的非正式定义。如果我们可以在需要超类型实例时使用子类型实例而无需更改代码,则两种类型处于子类型-超类型关系中。
This is an informal definition of the famous Liskov substitution principle. Two types are in a subtype-supertype relationship if we can use an instance of the subtype whenever an instance of the supertype is expected without having to change the code.
建立子类型关系有两种方式。大多数主流编程语言(如 Java 和 C#)使用的第一个称为名义子类型化。在名义子类型化中,如果我们使用像class Triangle extends Shape. Triangle现在我们可以在需要实例的时候使用实例Shape(例如函数的参数)。如果我们不声明Triangle为 extending Shape,编译器将不允许我们将它用作Shape.
There are two ways in which subtyping relationships are established. The first one, which most mainstream programming languages (such as Java and C#) use, is called nominal subtyping. In nominal subtyping, a type is the subtype of another type if we explicitly declare it as such, using syntax like class Triangle extends Shape. Now we can use an instance of Triangle whenever an instance of Shape is expected (such as as argument to a function). If we don’t declare Triangle as extending Shape, the compiler won’t allow us to use it as a Shape.
另一方面,结构子类型化不需要我们在代码中明确声明子类型化关系。可以使用类型的实例(例如Lbfs)代替另一种类型(例如 )Ns,只要它具有其他类型声明的所有成员即可。换句话说,如果一个类型与另一个类型具有相似的结构(相同的成员和可选的附加成员),它会自动被认为是另一个类型的子类型。
On the other hand, structural subtyping doesn’t require us to state the subtyping relationship explicitly in code. An instance of a type, such as Lbfs, can be used instead of another type, such as Ns, as long as it has all the members that the other type declares. In other words, if a type has a similar structure to another type (the same members and optionally additional members), it is automatically considered to be a subtype of that other type.
在名义子类型化中,如果我们明确声明一个类型,则该类型是另一种类型的子类型。在结构子类型化中,如果一个类型具有超类型的所有成员和可选的附加成员,则该类型是另一种类型的子类型。
In nominal subtyping, a type is a subtype of another type if we explicitly declare it as such. In structural subtyping, a type is a subtype of another type if it has all the members of the supertype and, optionally, additional members.
与 C# 和 Java 不同,TypeScript 使用结构子类型化。这就是为什么如果我们将Ns和声明为只有type 成员的Lbfs类,它们仍然可以互换使用。 valuenumber
Unlike C# and Java, TypeScript uses structural subtyping. That’s the reason why, if we declare Ns and Lbfs as classes with only a value member of type number, they can still be used interchangeably.
在许多情况下,结构子类型化很有用,因为它允许我们在类型之间建立关系,即使它们不在我们的控制之下。假设我们使用的库将User类型定义为具有name和age。在我们的代码中,我们有一个Named接口需要一个name实现类型的属性。我们可以在需要Usera 的任何时候使用一个实例Named,即使User没有显式实现Named,如下一个清单所示。(我们没有声明class User implements Named。)
In many cases, structural subtyping is useful, as it allows us to establish relationships between types even if they are not under our control. Suppose that a library we use defines a User type as having a name and age. In our code, we have a Named interface that requires a name property on implementing types. We can use an instance of User whenever a Named is expected, even though User does not explicitly implement Named, as shown in the next listing. (We don’t have the declaration class User implements Named.)
/* 库代码 */
用户类 { 1
名称:字符串;
年龄:数字;
构造函数(名称:字符串,年龄:数字){
this.name = 名称;
这个。年龄=年龄;
}
}
/* 我们的代码 */
接口命名为{
名称:字符串;
}
功能问候(命名:命名):void { 2
console.log(`嗨 ${named.name}!`);
}
问候(新用户(“爱丽丝”,25)); 3个/* Library code */
class User { 1
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
/* Our code */
interface Named {
name: string;
}
function greet(named: Named): void { 2
console.log(`Hi ${named.name}!`);
}
greet(new User("Alice", 25)); 3
如果我们必须显式声明Userimplements Named,我们就会遇到麻烦,因为User它是一个来自外部库的类型。我们不能更改库代码,因此我们必须通过声明一个扩展User和实现Named( class NamedUser extends User implements Named {}) 的新类型来解决这种情况,只是为了连接这两种类型。如果我们的类型系统使用结构子类型,我们不需要这样做。
If we had to explicitly declare that User implements Named, we would be in trouble, because User is a type that comes from an external library. We can’t change library code, so we would have to work around this situation by declaring a new type that extends User and implements Named (class NamedUser extends User implements Named {}) just to connect the two types. We don’t need to do this if our type system uses structural subtyping.
另一方面,在某些情况下,我们绝对不希望一个类型仅仅基于其结构就被认为是另一个类型的子类型。例如,Lbfs绝不能使用实例代替实例。Ns在名义子类型中,这是默认值,这使得避免错误变得非常容易。另一方面,结构子类型要求我们做更多的工作来确保一个值是我们期望的类型,而不是具有相似形状的类型的值。在这种情况下,结构子类型化要好得多。
On the other hand, in some situations we absolutely don’t want a type to be considered a subtype of another type based simply on its structure. A Lbfs instance should never be used instead of a Ns instance, for example. In nominal subtyping, this is the default, which makes it very easy to avoid mistakes. On the other hand, structural subtyping requires us to do more work to ensure that a value is of the type we expect it to be rather than a value of a type with a similar shape. In such scenarios, structural subtyping is much better.
如果我们想使用名义子类型化,我们可以使用多种技术在 TypeScript 中强制执行它。其中之一就是unique symbol我们在整本书中使用的技巧。让我们放大它。
If we want to use nominal subtyping, we can use several techniques to enforce it in TypeScript. One of them is the unique symbol trick we’ve used throughout the book. Let’s zoom in on it.
在我们的Ns/Lbfs案例中,我们正在有效地尝试模拟名义子类型化。我们希望确保编译器Ns仅在我们显式声明一个类型时才将其视为子类型,而不仅仅是因为它有一个value成员。
In our Ns/Lbfs case, we are effectively trying to simulate nominal subtyping. We want to make sure that the compiler considers a type to be a subtype of Ns only if we explicitly declare it as such, not just because it has a value member.
为此,我们需要向其中添加一个Ns其他类型无法意外声明的成员。在 TypeScript 中,unique symbol生成保证在所有代码中唯一的“名称”。不同的unique symbol声明将生成不同的名称,并且用户声明的名称永远不会与生成的名称相匹配。
To achieve this, we need to add a member to Ns that no other type can declare accidentally. In TypeScript, unique symbol generates a “name” that’s guaranteed to be unique across all the code. Different unique symbol declarations will generate different names, and no user-declared name can ever match a generated name.
我们声明一个唯一的符号来表示我们的Ns类型为NsType. 唯一符号声明如下所示:(declare const NsType: unique symbol如清单 7.1 所示)。现在我们有了一个唯一的名称,我们可以通过将名称放在方括号中来创建具有该名称的属性。我们需要为这个属性定义一个类型,但我们实际上并不打算给它分配任何东西,因为我们只是用它来消除类型歧义。因为我们不关心它的实际值,单位类型最适合这个目的,所以我们使用void.
We declare a unique symbol to represent our Ns type as NsType. The unique symbol declaration looks like this: declare const NsType: unique symbol (as in listing 7.1). Now that we have a unique name, we can create a property with that name by putting the name in square brackets. We need to define a type for this property, but we aren’t really going to assign anything to it because we’re just using it to disambiguate types. Because we don’t care about its actual value, a unit type is best suited for this purpose, so we use void.
我们对 做同样的事情Lbfs,现在类型有不同的结构:一个有一个[NsType]属性,另一个有一个[LbfsType]属性,如图在清单 7.4中。因为我们使用了unique symbol,所以不可能不小心在另一个类型上定义同名的属性。Ns为和now提出子类型的唯一方法Lbfs是显式继承它们。
We do the same for Lbfs, and now the types have different structures: one of them has a [NsType] property, and the other has a [LbfsType] property, as shown in listing 7.4. Because we used unique symbol, it’s impossible to accidentally define a property with the same name on another type. The only way to come up with a subtype for Ns and Lbfs now is to explicitly inherit from them.
声明 const NsType:唯一符号;
类 Ns {
值:数字;
[NsType]: void;
构造函数(值:数字){
this.value = 值;
}
}
声明 const LbfsType:唯一符号;
类 Lbfs {
值:数字;
[LbfsType]: void;
构造函数(值:数字){
this.value = 值;
}
}
函数 acceptNs(momentum: Ns): void {
console.log(`Momentum: ${momentum.value} Ns`);
}
acceptNs(新 Lbfs(10)); 1个declare const NsType: unique symbol;
class Ns {
value: number;
[NsType]: void;
constructor(value: number) {
this.value = value;
}
}
declare const LbfsType: unique symbol;
class Lbfs {
value: number;
[LbfsType]: void;
constructor(value: number) {
this.value = value;
}
}
function acceptNs(momentum: Ns): void {
console.log(`Momentum: ${momentum.value} Ns`);
}
acceptNs(new Lbfs(10)); 1
当我们尝试将Lbfs实例作为 a传递时Ns,我们会收到以下错误:
When we try to pass a Lbfs instance as a Ns, we get the following error:
“Lbfs”类型的参数不可分配给参数 输入“Ns”。“Lbfs”类型中缺少属性“[NsType]” 但在类型“Ns”中是必需的。
Argument of type 'Lbfs' is not assignable to parameter of type 'Ns'. Property '[NsType]' is missing in type 'Lbfs' but required in type 'Ns'.
在本节中,我们看到了子类型化的定义,并了解了可以在两种类型之间建立子类型化关系的两种方式:名义上(因为我们这么说)和结构上(因为类型具有相同的结构)。我们还看到,即使 TypeScript 使用结构子类型,我们也可以通过在结构子类型不合适的情况下使用唯一符号来模拟名义子类型。
In this section, we saw a definition of subtyping and learned about the two ways in which the subtyping relationship between two types can be established: nominally (because we say so) and structurally (because the types have the same structure). We also saw how, even though TypeScript uses structural subtyping, we can simulate nominal subtyping by using unique symbols for the situations in which structural subtyping is not appropriate.
在 TypeScript 中,是定义为的类型 Painting的子类型Wine
类酒{ 名称:字符串; 年份:数字; } 类绘画{ 名称:字符串; 年份:数字; 画家:画家; }
In TypeScript, is Painting a subtype of Wine for the types defined as
class Wine { name: string; year: number; } class Painting { name: string; year: number; painter: Painter; }
在 TypeScript 中,是定义为的类型 Car的子类型Wine
类酒{ 名称:字符串; 年份:数字; } 类车{ 制作:字符串; 模型:字符串; 年份:数字; }
In TypeScript, is Car a subtype of Wine for the types defined as
class Wine { name: string; year: number; } class Car { make: string; model: string; year: number; }
现在我们已经了解了子类型化,让我们看看两个极端:一个我们可以分配任何东西的类型和一个我们可以分配给任何东西的类型。第一个是我们可以用来存储任何东西的类型。第二种是我们可以使用的类型,如果我们没有其他类型的实例的话,我们可以使用它来代替任何其他类型。
Now that we’ve learned about subtyping, let’s look at a couple of extremes: a type to which we can assign anything and a type that we can assign to anything. The first one is a type we can use to store absolutely anything. The second is a type we can use instead of any other type if we don’t have an instance of that other type handy.
我们在第 4 章unknown介绍了和类型。是一种可以存储任何其他类型的值的类型。我们提到其他面向对象的语言通常会提供一种以类似行为命名的类型。事实上,TypeScript也有类型;它提供了一些常用方法,例如. 但故事并没有就此结束,正如我们将在本节中看到的那样。 anyunknownObjectObjecttoString()
We covered the unknown and any types in chapter 4. unknown is a type that can store a value of any other type. We mentioned that other object-oriented languages usually provide a type named Object with similar behavior. In fact, TypeScript has an Object type too; it provides a few common methods such as toString(). But the story doesn’t end there, as we’ll see in this section.
该any类型更危险。我们不仅可以给它赋值,还可以any给任何其他类型赋值,绕过类型检查。此类型用于与 JavaScript 代码的互操作性,但可能会产生意想不到的后果。假设我们有一个使用标准反序列化对象的函数JSON.parse(),如下一个清单所示。因为JSON.parse()它是一个 JavaScript 函数,它与 TypeScript 互操作,所以它不是强类型的;它的返回类型是any. 假设我们期望反序列化一个User具有name属性的实例。
The any type is more dangerous. We can not only assign any value to it, but also assign an any value to any other type, bypassing type checking. This type is used for interoperability with JavaScript code but may have unintended consequences. Suppose that we have a function that deserializes an object using the standard JSON.parse(), as shown in the next listing. Because JSON.parse() is a JavaScript function with which TypeScript interoperates, it is not strongly typed; its return type is any. Assume that we are expecting to deserialize a User instance that has a name property.
类用户{
名称:字符串; 1个
构造函数(名称:字符串){
this.name = 名称;
}
}
函数反序列化(输入:字符串):任何{
返回 JSON.parse(输入); 2个
}
功能问候(用户:用户):void {
console.log(`嗨 ${user.name}!`); 3个
}
问候(反序列化('{“名称”:“爱丽丝”}')); 4
问候(反序列化('{}')); 5个class User {
name: string; 1
constructor(name: string) {
this.name = name;
}
}
function deserialize(input: string): any {
return JSON.parse(input); 2
}
function greet(user: User): void {
console.log(`Hi ${user.name}!`); 3
}
greet(deserialize('{ "name": "Alice" }')); 4
greet(deserialize('{}')); 5
最后一次调用greet()将记录日志"Hi undefined!",因为any绕过了类型检查,并且编译器允许我们将返回值视为 type 的值User,即使我们没有获得该类型的值。这个结果显然并不理想。我们需要在调用之前检查类型是否正确greet()。
The last call to greet() will log "Hi undefined!" because any bypasses type checking, and the compiler allows us to treat the returned value as a value of type User, even when we didn’t get a value of that type. This result is clearly not ideal. We need to check that we have the right type before we call greet().
在这种情况下,我们要确保我们拥有的对象具有nametype 的属性string,在我们的情况下足以将其转换为User. 我们还应该检查我们的对象不是nullor undefined,它们是 TypeScript 中的特殊类型。这样做的一种方法是用这样的检查更新我们的代码,并在调用之前调用它greet()。请注意,此类型检查是在运行时完成的,因为它取决于输入值并且不是可以静态强制执行的内容。
In this case, we’d want to ensure that the object we have has a name property of type string, which in our case is enough to cast it into a User. We should also check that our object is not null or undefined, which are special types in TypeScript. One way of doing this is to update our code with such a check and call it before calling greet(). Note that this type check is done at run time, because it depends on the input value and is not something that can be enforced statically.
类用户{
名称:字符串;
构造函数(名称:字符串){
this.name = 名称;
}
}
函数反序列化(输入:字符串):任何{
返回 JSON.parse(输入);
}
功能问候(用户:用户):void {
console.log(`嗨 ${user.name}!`);
}
function isUser(user: any): user is User { 1
if (user === null || user === undefined)
return false;
return typeof user.name === 'string';
}
let user: any = deserialize('{ "name": "Alice" }');
如果(是用户(用户)) 2
问候(用户);
用户 = 未定义;
如果(isUser(用户)) 2
问候(用户);class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
function deserialize(input: string): any {
return JSON.parse(input);
}
function greet(user: User): void {
console.log(`Hi ${user.name}!`);
}
function isUser(user: any): user is User { 1
if (user === null || user === undefined)
return false;
return typeof user.name === 'string';
}
let user: any = deserialize('{ "name": "Alice" }');
if (isUser(user)) 2
greet(user);
user = undefined;
if (isUser(user)) 2
greet(user);
的user is User返回类型isUser()是一些特定于 TypeScript 的语法,但我希望它不会太混乱。这种类型非常像boolean返回类型,但它对编译器具有额外的意义。如果函数返回true,则变量的user类型为User,编译器可以在调用方中使用该信息。实际上,在返回的每个if块中,具有类型而不是。 isUser()trueuserUserany
The user is User return type of isUser() is a bit of TypeScript-specific syntax, but I hope that it’s not too confusing. This type is very much like a boolean return type, but it carries extra meaning for the compiler. If the function returns true, the variable user has type User, and the compiler can use that information in the caller. Effectively, within each if block in which isUser() returned true, user has type User instead of any.
这种方法有效。当我们的用户名是 Alice 时,运行代码只会执行第一次调用。不会执行第二次调用,greet()因为在这种情况下,没有name属性user。但是,这种方法仍然存在一个问题:我们不是被迫执行此检查。因为没有强制执行,我们可能会犯错误而忘记调用它,这将允许任意结果从deserialize()进入greet(),并且没有什么可以阻止它这样做。
This approach works. Running the code executes only the first call when our username is Alice. The second call to greet() will not be executed because in this case, there is no name property on user. There’s still a problem with this approach, though: we are not forced to implement this check. Because no enforcement is going on, we could make a mistake and forget to call it, which would allow an arbitrary result from deserialize() to make its way to greet(), and there’s nothing to stop it from doing so.
如果我们有另一种说法,“这个对象绝对可以是任何类型”,但没有额外的“相信我,我知道我在做什么”暗示,那不是很好吗any?我们需要另一种类型——一种是系统中任何其他类型的超类型的类型,这意味着无论JSON.parse()返回什么,它都将是该类型的子类型。从那时起,类型系统将确保我们在将其转换为User.
Wouldn’t it be great if we had another way of saying, “This object can be of absolutely any type” but without the additional “Trust me, I know what I’m doing” that any implies? We need another type—a type that is a supertype of any other type in the system, which means that regardless of what JSON.parse()returns, it will be a subtype of this type. From there on, the type system will ensure that we add the proper type checking before we cast it to User.
我们可以为其分配任何值的类型也称为顶级类型,因为任何其他类型都是该类型的子类型。换句话说,这种类型位于子类型层次结构的顶部(图 7.1)。
A type to which we can assign any value is also called a top type because any other type is a subtype of this type. In other words, this type sits at the top of the subtyping hierarchy (figure 7.1).
让我们更新我们的实现。我们可以从类型开始,它是类型系统中大多数Object类型的超类型,但有两个例外:和。TypeScript 类型系统具有一些强大的安全特性,其中之一就是能够将和值保留在其他类型的域之外。请记住第 3 章中的数十亿美元错误侧边栏——在大多数语言中,我们可以分配给任何类型。如果我们使用编译器标志(强烈推荐),这在 TypeScript 中是不允许的。TypeScript 认为是类型and 。所以我们的顶级类型,绝对是任何东西的超类型,是这三种类型的总和:nullundefinednullundefinednull--strictNullChecksnullnullundefined是类型undefinedObject | null | undefined. 这种类型实际上开箱即用地定义为unknown. 让我们重写代码以使用unknown,如下一个清单所示,然后我们可以讨论 usingany和之间的区别unknown。
Let’s update our implementation. We can start with the Object type, which is the supertype of most types in the type systems, with two exceptions: null and undefined. The TypeScript type system has some great safety features, one of them being the ability to keep null and undefined values outside the domain of other types. Remember the billion-dollar-mistake sidebar in chapter 3—the fact that in most languages, we can assign null to any type. This is not allowed in TypeScript if we use the --strictNullChecks compiler flag (which is strongly recommended). TypeScript considers null to be of type null and undefined to be of type undefined. So our top type, the supertype of absolutely anything, is the sum of these three types: Object | null | undefined. This type is actually defined out of the box as unknown. Let’s rewrite our code to use unknown, as shown in the next listing, and then we can discuss the differences between using any and unknown.
类用户{
名称:字符串;
构造函数(名称:字符串){
this.name = 名称;
}
}
函数反序列化(输入:字符串):未知{ 1
返回 JSON.parse(输入);
}
功能问候(用户:用户):void {
console.log(`嗨 ${user.name}!`);
}
function isUser(user: any): 用户是用户 { 2
如果(用户 === 空 || 用户 === 未定义)
返回假;
return typeof user.name === 'string';
}
让用户:未知=反序列化('{“名称”:“爱丽丝”}'); 3个
如果(是用户(用户))
问候(用户);
用户=反序列化(“空”);
如果(是用户(用户))
问候(用户);class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
function deserialize(input: string): unknown { 1
return JSON.parse(input);
}
function greet(user: User): void {
console.log(`Hi ${user.name}!`);
}
function isUser(user: any): user is User { 2
if (user === null || user === undefined)
return false;
return typeof user.name === 'string';
}
let user: unknown = deserialize('{ "name": "Alice" }'); 3
if (isUser(user))
greet(user);
user = deserialize("null");
if (isUser(user))
greet(user);
这种变化是微妙但强大的:一旦我们从 获得一个值JSON.parse(),我们就将它从 转换any为unknown。这个过程是安全的,因为任何东西都可以转换为unknown. isUser()我们保留as的参数any,因为它使我们的实现更容易。(我们不允许typeof user.name在没有额外转换的情况下执行诸如 an之类的检查unknown。)
The change is subtle but powerful: as soon as we get a value from JSON.parse(), we convert it from any to unknown. This process is safe, because anything can be converted to unknown. We keep the argument of isUser() as any, because it makes our implementation easier. (We wouldn’t be allowed to perform a check such as typeof user.name on an unknown without extra casting.)
代码像以前一样工作,区别在于如果我们删除任何调用isUser(),代码将不再编译。编译器发出以下错误:
The code works as before, the distinction being that if we remove any of the isUser() calls, the code no longer compiles. The compiler issues the following error:
“未知”类型的参数不可分配给参数 类型为“用户”。
Argument of type 'unknown' is not assignable to parameter of type 'User'.
我们不能简单地将类型变量传递给unknown,greet()它需要一个User. 该函数isUser()很有帮助,因为每当它返回时true,编译器都会自动将变量视为类型User。
We can’t simply pass a variable of type unknown to greet(), which expects a User. The function isUser() helps, as whenever it returns true, the compiler automatically considers the variable to have type User.
有了这个实现,我们就不能忘记检查;编译器不允许我们。它允许我们仅User在确认user is User.
With this implementation, we simply cannot forget to check; the compiler will not allow us. It allows us to use our object as a User only after we confirm that user is User.
unknown尽管我们可以为和分配任何内容any,但是我们使用其中一种类型的变量的方式有所不同。在这种情况下,只有在我们确认该值确实具有该类型(就像我们对将用户返回为 的函数所做的那样)之后unknown,我们才能将该值用作某种类型(例如 )。在这种情况下,我们可以立即将该值用作任何其他类型的值。绕过类型检查。 UserUseranyany
Although we can assign anything to both unknown and any, there is a difference in how we use a variable of one of these types. In the unknown case, we can use the value as some type (such as User) only after we confirm that the value actually has that type (as we did with the function that returns the user as User). In the any case, we can use the value as a value of any other type right away. any bypasses type checking.
其他语言提供不同的机制来确定值是否属于给定类型。is例如,C# 有关键字,而 Java 有instanceof. 通常,当我们处理可以是任何值的值时,我们首先将其视为顶级类型。然后我们使用适当的检查来确保它是我们需要的类型,然后再将其向下转换为所需的类型。
Other languages provide different mechanisms to determine whether a value is of a given type. C# has the is keyword, for example, and Java has instanceof. In general, when we deal with a value that could be anything, we start by considering it to be a top type. Then we use the appropriate checks to ensure that it is of the type we need before we downcast it to the required type.
现在让我们看一个相反的问题:一个可以代替任何其他类型使用的类型。让我们以清单 7.8中的一个简单示例为例。在我们的游戏中,我们可以转动我们的宇宙飞船Left或Right。我们会将这些可能的方向表示为枚举。我们想要实现一个函数,它接受一个方向并将其转换为我们旋转宇宙飞船的角度。因为我们要确保涵盖所有情况,所以如果枚举的值与两个预期值Left和Right值不同,我们将抛出错误。
Now let’s look at an opposite problem: a type that can be used instead of any other type. Let’s take a simple example in listing 7.8. In our game, we can turn our spaceship Left or Right. We’ll represent these possible directions as an enumeration. We want to implement a function that takes a direction and converts it to an angle by which we rotate our spaceship. Because we want to make sure that we cover all cases, we’ll throw an error if the enumeration has a value different from the two expected Left and Right values.
枚举转弯方向 {
左边,
正确的
}
函数 turnAngle(turn: TurnDirection): number {
开关(转){
case TurnDirection.Left: 返回 -90; 1
例 TurnDirection.Right:返回 90; 1
默认值:throw new Error("Unknown TurnDirection"); 2个
}
}enum TurnDirection {
Left,
Right
}
function turnAngle(turn: TurnDirection): number {
switch (turn) {
case TurnDirection.Left: return -90; 1
case TurnDirection.Right: return 90; 1
default: throw new Error("Unknown TurnDirection"); 2
}
}
到目前为止,一切都很好。但是如果我们有一个处理错误场景的函数呢?假设我们想在抛出错误之前记录错误。这个函数总是会抛出异常,所以我们将它声明为返回类型never,正如我们在第 2 章中看到的那样。提醒一下,never是不能赋值的空类型。我们用它来明确地表明一个函数永远不会返回,要么是因为它永远循环,要么是因为它抛出异常,如下一个清单所示。
So far, so good. But what if we have a function that handles error scenarios? Suppose that we want to log the error before throwing it. This function would always throw, so we’ll declare it as returning the type never, as we saw in chapter 2. As a reminder, never is the empty type that cannot be assigned any value. We use it to explicitly show that a function never returns, either because it loops forever or because it throws, as shown in the next listing.
function fail(message: string): never { 1
console.error(message); 2
抛出新的错误(消息); 2
}function fail(message: string): never { 1
console.error(message); 2
throw new Error(message); 2
}
如果我们想用 替换语句throw,我们最终会得到类似下面的内容。 turnAngle()fail()
If we want to replace the throw statement in turnAngle() with fail(), we end up with something like the following.
函数 turnAngle(turn: TurnDirection): number {
开关(转){
case TurnDirection.Left: 返回 -90;
case TurnDirection.Right: 返回 90;
默认值:失败(“未知转向”); 1个
}
}function turnAngle(turn: TurnDirection): number {
switch (turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: fail("Unknown TurnDirection"); 1
}
}
这段代码几乎可以工作,但不完全是。在严格模式下(带--strict标志)编译失败,出现以下错误:
This code almost works, but not quite. Compilation fails in strict mode (with --strict flag) with the following error:
函数缺少结束返回语句和返回类型 不包括“未定义”。
Function lacks ending return statement and return type does not include "undefined".
编译器看不到分支return上的语句default并将其标记为错误。一个修复方法是返回一个虚拟值,如下一个清单所示,知道我们无论如何都会在到达它之前抛出。
The compiler doesn’t see a return statement on the default branch and flags that as an error. One fix would be to return a dummy value as shown in the next listing, knowing that we throw before reaching it anyway.
枚举转弯方向 {
左边,
正确的
}
函数 turnAngle(turn: TurnDirection): number {
开关(转){
case TurnDirection.Left: 返回 -90;
case TurnDirection.Right: 返回 90;
默认: {
失败(“未知转向”);
返回-1; 1个
}
}
}enum TurnDirection {
Left,
Right
}
function turnAngle(turn: TurnDirection): number {
switch (turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: {
fail("Unknown TurnDirection");
return -1; 1
}
}
}
但是,如果在未来的某个时候,我们fail()以一种它并不总是抛出的方式更新呢?那么我们的代码最终会返回一个虚拟值,即使它永远不会这样做。有一个更好的解决方案:返回 的结果fail(),如以下清单所示。
But what if, at some point in the future, we update fail()in such a way that it doesn’t always throw? Then our code would end up returning a dummy value, even though it should never do so. There’s a better solution: return the result of fail(), as the following listing shows.
函数 turnAngle(turn: TurnDirection): number {
开关(转){
case TurnDirection.Left: 返回 -90;
case TurnDirection.Right: 返回 90;
默认值:return fail("Unknown TurnDirection"); 1个
}
}function turnAngle(turn: TurnDirection): number {
switch (turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: return fail("Unknown TurnDirection"); 1
}
}
这段代码之所以有效,是因为除了是没有值的类型之外,never该类型还是系统中所有其他类型的子类型。
The reason why this code works is that besides being the type without values, never is the type that is the subtype of all other types in the system.
作为任何其他类型的子类型的类型称为底部类型,因为它位于子类型层次结构的底部。要成为任何其他可能类型的子类型,它必须具有任何其他可能类型的成员。因为我们可以有无限数量的类型和成员,所以底层类型也必须有无限数量的成员。因为那是不可能的,底层类型总是一个空类型:我们不能为其创建实际值的类型(图 7.2)。
A type that is the subtype of any other type is called a bottom type because it sits at the bottom of the subtyping hierarchy. To be a subtype of any other possible type, it must have the members of any other possible type. Because we can have an infinite number of types and members, the bottom type would also have to have an infinite number of members. Because that is impossible, the bottom type is always an empty type: a type for which we can’t create an actual value (figure 7.2).
因为我们可以分配never给任何其他类型,因为它是底层类型,所以我们可以从函数中返回它。编译器不会抱怨,因为这是一个向上转换(将值从子类型转换为超类型),它可以隐式完成。我们说,“把这个不可能创建的值变成一个字符串,”这很好。因为该fail()函数永远不会返回,所以我们永远不会遇到实际需要将某些内容转换为字符串的情况。
Because we can assign never to any other type, due to it being a bottom type, we can return it from the function. The compiler will not complain, as this is an upcast (converting a value from a subtype to a supertype), which can be done implicitly. We’re saying, “Take this value that is impossible to create and turn it into a string,” which is fine. Because the fail() function never returns, we never end up in a situation in which we actually have something to turn into a string.
这种方法比前一种方法更好,因为如果我们更新fail()以致在某些情况下它不更新throw,编译器将强制我们修复所有代码。第一的,fail()它会迫使我们将from的返回类型更改never为其他类型,例如void. 然后它会看到我们正在尝试将其作为 a 传递string,它不进行类型检查。我们将不得不更新我们的实现turnAngle(),也许是通过恢复显式throw.
This approach is better than the preceding one because, if we update fail() so that it doesn’t throw in some cases, the compiler will force us to fix all our code. First, it will force us to change the return type of fail() from never to something else, such as void. Then it will see that we are trying to pass that as a string, which does not type-check. We will have to update our implementation of turnAngle(), perhaps by bringing back the explicit throw.
底部类型允许我们假装我们有任何类型的值,即使我们不能想出一个。
A bottom type allows us to pretend that we have a value of any type even if we can’t come up with one.
让我们快速回顾一下我们在本节中介绍的内容。两种类型可以处于子类型化关系中,其中一个是超类型,另一个是子类型。在极端情况下,我们有一个类型是任何其他类型的超类型和一个类型是任何其他类型的子类型。
Let’s quickly recap what we covered in this section. Two types can be in a subtyping relationship, in which one of them is the supertype and the other is the subtype. At the extreme, we have a type that is the supertype of any other type and a type that is the subtype of any other type.
任何其他类型的超类型,称为顶级类型,可用于保存任何其他类型的值。该类型unknown在 TypeScript 中。这会派上用场的一种情况是,当我们处理可以是任何数据的数据时,例如从 NoSQL 数据库读取的 JSON 文档。我们最初将此类数据键入顶级类型,然后执行所需的检查以将其转换为我们可以使用的类型。
The supertype of any other type, called the top type, can be used to hold a value of any other type. That type is unknown in TypeScript. One situation in which this comes in handy is when we are dealing with data that can be anything, such as as a JSON document read from a NoSQL database. We initially type such data as the top type and then perform the required checks to cast it down to a type we can work with.
任何其他类型的子类型,称为底层类型,可用于产生任何其他类型的值。这种类型never在 TypeScript 中。一个示例应用程序在无法通过始终抛出的函数生成返回值时生成返回值。
The subtype of any other type, called the bottom type, can be used to produce a value of any other type. This type is never in TypeScript. One example application is producing a return value when none can be produced via a function that always throws.
请注意,尽管大多数主流语言都提供顶层类型,但很少有主流语言提供底层类型。我们在第 2 章中看到的 DIY 实现使类型为空但不为底部。除非进入编译器,否则无法定义我们的自定义底部类型。
Note that although most mainstream languages provide a top type, few of them provide a bottom type. The DIY implementation we saw in chapter 2 makes a type empty but not bottom. Unless worked into the compiler, there is no way to define our custom bottom type.
接下来,让我们看看更复杂类型的子类型化,看看它是如何工作的。
Next, let’s look at subtyping for more complex types and see how that works.
如果我们有一个makeNothing()返回 的函数never,我们可以用它的结果初始化一个x类型的变量吗number(不强制转换)?
声明函数 makeNothing(): 从不; 让 x: number = makeNothing();
If we have a function makeNothing() that returns never, can we initialize a variable x of type number with its result (without casting)?
declare function makeNothing(): never; let x: number = makeNothing();
如果我们有一个makeSomething()返回 的函数unknown,我们可以用它的结果初始化一个x类型的变量吗number(不强制转换)?
声明函数 makeSomething(): 未知; 让 x: number = makeSomething();
If we have a function makeSomething() that returns unknown, can we initialize a variable x of type number with its result (without casting)?
declare function makeSomething(): unknown; let x: number = makeSomething();
到目前为止,我们已经看过几个简单的子类型示例。例如,我们观察到如果Triangle extends Shape,Triangle是 的子类型Shape。现在让我们尝试回答一些更棘手的问题:
So far, we’ve looked at a few simple examples of subtyping. We observed, for example, that if Triangle extends Shape, Triangle is a subtype of Shape. Now let’s try to answer a few trickier questions:
这些问题很重要,因为它们告诉我们这些类型中的哪些可以替代它们的子类型。每当我们看到一个需要这些类型之一的参数的函数时,我们应该了解我们是否可以提供一个子类型。
These questions are important, as they tell us which of these types can be substituted for their subtypes. Whenever we see a function that expects an argument of one of these types, we should understand whether we can provide a subtype instead.
前面示例中的挑战在于事情并不像Triangle extends Shape. 我们正在研究基于Triangle和定义的类型Shape。Triangle并且Shape是总和类型、集合元素类型或函数参数类型或返回类型的一部分。
The challenge in the preceding examples is that things aren’t as straightforward as Triangle extends Shape. We are looking at types that are defined based on Triangle and Shape. Triangle and Shape are part of the sum types, the types of elements of a collection, or a function’s argument types or return types.
我们先举一个最简单的例子:sum 类型。假设我们有一个draw()可以绘制 a Triangle、 aSquare或 a 的函数Circle。我们可以传递 a TriangleorSquare给它吗?正如您可能已经猜到的那样,答案是肯定的。我们可以在下面的清单中检查这样的代码是否编译。
Let’s take the simplest example first: the sum type. Suppose that we have a draw() function that can draw a Triangle, a Square, or a Circle. Can we pass a Triangle or Square to it? As you might have guessed, the answer is yes. We can check that such code compiles in the following listing.
声明 const TriangleType:唯一符号;
类三角形{
[三角形类型]: void;
/* 三角成员 */
}
声明 const SquareType:唯一符号;
类广场{
[方形]: void;
/* 方形成员 */
}
声明 const CircleType:唯一符号;
类圈子{
[圆类型]: void;
/* 圈子成员 */
}
声明函数 makeShape(): 三角形 | 正方形; 1
声明函数 draw(shape: Triangle | Square | Circle): void; 2个
绘制(制作形状());declare const TriangleType: unique symbol;
class Triangle {
[TriangleType]: void;
/* Triangle members */
}
declare const SquareType: unique symbol;
class Square {
[SquareType]: void;
/* Square members */
}
declare const CircleType: unique symbol;
class Circle {
[CircleType]: void;
/* Circle members */
}
declare function makeShape(): Triangle | Square; 1
declare function draw(shape: Triangle | Square | Circle): void; 2
draw(makeShape());
我们在这些示例中强制执行标称子类型化,因为我们没有为这些类型提供完整的实现。在实践中,它们会有各种不同的属性和方法来区分它们。我们在示例中使用独特的符号来模拟这些不同的属性,因为由于 TypeScript 的结构子类型化,将类留空会使它们全部等效。
We enforce nominal subtyping throughout these examples because we’re not providing full implementations for these types. In practice, they would have various different properties and methods to distinguish them. We simulate these different properties with unique symbols for our examples, as leaving the classes empty would make all of them equivalent due to TypeScript’s structural subtyping.
正如预期的那样,此代码可以编译。反之则不然:如果我们可以绘制 aTriangle或 aSquare并尝试绘制 a Triangle, Square, or Circle,编译器会报错,因为我们可能最终将 a 传递Circle给draw()函数,而函数不知道如何处理它。我们可以确认以下代码无法编译。
As expected, this code compiles. The opposite doesn’t: if we can draw a Triangle or a Square and attempt to draw a Triangle, Square, or Circle, the compiler will complain, because we might end up passing a Circle to the draw() function, which wouldn’t know what to do with it. We can confirm that the following code doesn’t compile.
声明函数 makeShape():三角形 | 方形 | 圆圈; 1 声明函数 draw(shape: Triangle | Square ): void; 1个 绘制(制作形状()); 2个
declare function makeShape(): Triangle | Square | Circle; 1 declare function draw(shape: Triangle | Square): void; 1 draw(makeShape()); 2
Triangle | Square是 的子类型Triangle | Square | Circle:我们总是可以用 aTriangle或代替Squarea Triangle, Square, orCircle但不能反过来。
Triangle | Square is a subtype of Triangle | Square | Circle: we can always substitute a Triangle or Square for a Triangle, Square, or Circle but not the other way around.
这种情况似乎有悖常理,因为Triangle | Square“小于” Triangle | Square | Circle。每当我们使用继承时,我们最终都会得到一个比其超类型具有更多属性的子类型。对于 sum 类型,它以相反的方式工作:超类型比子类型具有更多类型(图 7.3)。
This situation may seem to be counterintuitive, because Triangle | Square is “less” than Triangle | Square | Circle. Whenever we use inheritance, we end up with a subtype that has more properties than its supertype. For sum types, it works the opposite way: the supertype has more types than the subtype (figure 7.3).
假设我们有一个EquilateralTriangle继承自 的Triangle,如下一个清单所示。
Say we have an EquilateralTriangle which inherits from Triangle, as shown in the next listing.
声明 const EquilateralTriangleType:唯一符号;
类 EquilateralTriangle 扩展三角形 {
[等边三角形类型]: void;
/* 等边三角形成员 */
}declare const EquilateralTriangleType: unique symbol;
class EquilateralTriangle extends Triangle {
[EquilateralTriangleType]: void;
/* EquilateralTriangle members */
}
作为练习,检查当我们将求和类型与继承混合时会发生什么。makeShape()退货EquilateralTriangle | Square和draw()接受Triangle | Square | Circle工作吗?makeShape()退货Triangle | Square和draw()接受怎么样EquilateralTriangle | Square | Circle?
As an exercise, check what happens when we mix sum types with inheritance. Does makeShape() returning EquilateralTriangle | Square and draw() accepting Triangle | Square | Circle work? What about makeShape() returning Triangle | Square and draw() accepting EquilateralTriangle | Square | Circle?
这种形式的子类型是编译器必须支持的。使用我们在第 3 章Variant中看到的DIY 求和类型,我们不会得到相同的子类型行为。请记住,可以包装多种类型之一的值,但它本身不是这些类型中的任何一种。 Variant
This form of subtyping is something that has to be supported by the compiler. With a DIY sum type like the Variant we looked at in chapter 3, we would not get the same subtyping behavior. Remember the Variant can wrap a value of one of several types, but it is not itself any of those types.
现在让我们看看包含一些其他类型的一组值的类型。让我们从下一个清单中的数组开始。如果是 的子类型,我们可以将Triangle对象数组传递给接受对象draw()数组的函数吗? ShapeTriangleShape
Now let’s look at types that contain a set of values of some other type. Let’s start with arrays in the next listing. Can we pass an array of Triangle objects to a draw() function that accepts an array of Shape objects if Triangle is a subtype of Shape?
类形状{
/* 形状成员 */
}
声明 const TriangleType:唯一符号;
三角形类扩展形状 { 1
[三角形类型]: void;
/* 三角成员 */
}
声明函数 makeTriangles(): Triangle[]; 2
声明函数 draw(shapes: Shape[]): void; 3个
绘制(制作三角形()); 4个class Shape {
/* Shape members */
}
declare const TriangleType: unique symbol;
class Triangle extends Shape { 1
[TriangleType]: void;
/* Triangle members */
}
declare function makeTriangles(): Triangle[]; 2
declare function draw(shapes: Shape[]): void; 3
draw(makeTriangles()); 4
这种观察可能并不令人惊讶,但它很重要:数组保留了它们所存储的基础类型的子类型关系。正如预期的那样,相反的情况不起作用:如果我们在需要对象Shape数组时尝试传递对象数组Triangle,代码将无法编译(图 7.4)。
This observation may not be surprising, but it is important: arrays preserve the subtyping relationship of the underlying types that they are storing. As expected, the opposite doesn’t work: if we try to pass an array of Shape objects when an array of Triangle objects is expected, the code won’t compile (figure 7.4).
正如我们在第 2 章中看到的,数组是许多编程语言中现成的基本类型。如果我们定义一个自定义集合,比如一个LinkedList<T>?
As we saw in chapter 2, arrays are basic types that come out of the box in many programming languages. What if we define a custom collection, such as a LinkedList<T>?
类 LinkedList<T> { 1
值:T;
下一个:链表<T> | 未定义=未定义;
构造函数(值:T){
this.value = 值;
}
追加(值:T):LinkedList<T> {
this.next = new LinkedList(value);
返回这个。下一个;
}
}
声明函数 makeTriangles(): LinkedList<Triangle>; 2
声明函数 draw(shapes: LinkedList<Shape>): void; 3个
绘制(制作三角形()); 4个class LinkedList<T> { 1
value: T;
next: LinkedList<T> | undefined = undefined;
constructor(value: T) {
this.value = value;
}
append(value: T): LinkedList<T> {
this.next = new LinkedList(value);
return this.next;
}
}
declare function makeTriangles(): LinkedList<Triangle>; 2
declare function draw(shapes: LinkedList<Shape>): void; 3
draw(makeTriangles()); 4
即使没有基本类型,TypeScript 也会正确地确定它LinkedList-<Triangle>是LinkedList<Shape>. 和以前一样,相反的不会编译;我们不能将 aLinkedList<Shape>作为 a传递LinkedList<Triangle>。
Even without a primitive type, TypeScript correctly establishes that LinkedList-<Triangle> is a subtype of LinkedList<Shape>. As before, the opposite doesn’t compile; we can’t pass a LinkedList<Shape> as a LinkedList<Triangle>.
保留其基础类型的子类型化关系的类型称为协变。数组是协变的,因为它保留了子类型关系:Triangle是 的子类型Shape,也是Triangle[]的子类型Shape[]。
A type that preserves the subtyping relationship of its underlying type is called covariant. An array is covariant because it preserves the subtyping relationship: Triangle is a subtype of Shape, so Triangle[] is a subtype of Shape[].
在处理数组和集合(例如LinkedList<T>. 例如,在 C# 中,我们必须显式声明类型的协变性,例如LinkedList<T>通过声明接口和使用out关键字 ( ILinkedList<out T>)。否则,编译器将不会推导出子类型关系。
Various languages behave differently when dealing with arrays and collections such as LinkedList<T>. In C#, for example, we would have to explicitly state covariance for a type such as LinkedList<T> by declaring an interface and using the out keyword (ILinkedList<out T>). Otherwise, the compiler will not deduce the subtyping relationship.
协变的替代方法是简单地忽略两个给定类型之间的子类型关系,并考虑 aLinkedList<Shape>和LinkedList<Triangle>是它们之间没有子类型关系的类型。(两者都不是另一个的子类型。)在 TypeScript 中不是这种情况,但在 C# 中是这样,其中 aList<Shape>和 aList<Triangle>没有子类型关系。
An alternative to covariance is to simply ignore the subtyping relationship between two given types and consider a LinkedList<Shape> and LinkedList<Triangle> to be types with no subtyping relationship between them. (Neither is a subtype of the other.) This is not the case in TypeScript, but it is in C#, in which a List<Shape> and a List<Triangle> have no subtyping relationship.
忽略其基础类型的子类型化关系的类型称为不变的。AC#List<T>是不变的,因为它忽略了子类型关系"Triangle is a subtype of Shape",因此List<Shape>和List<Triangle>没有子类型-超类型关系。
A type that ignores the subtyping relationship of its underlying type is called invariant. A C# List<T> is invariant because it ignores the subtyping relationship "Triangle is a subtype of Shape", so List<Shape> and List<Triangle> have no subtype–supertype relationship.
现在我们已经了解了集合如何在子类型方面相互关联,并且已经看到了两种常见的差异类型,让我们看看函数类型是如何相关的。
Now that we’ve looked at how collections relate to one another in terms of subtyping and have seen two common types of variance, let’s see how function types are related.
Triangle我们首先从更简单的情况开始:让我们看看我们可以在返回 a 的函数和返回 a 的函数之间进行哪些替换Shape,如清单 7.18所示。我们将声明两个工厂函数:makeShape()返回 a 的 aShape和makeTriangle()返回 a 的 a Triangle。
We’ll start with the simpler case first: let’s see what substitutions we can make between a function that returns a Triangle and a function that returns a Shape, as shown in listing 7.18. We’ll declare two factory functions: a makeShape() that returns a Shape and a makeTriangle() that returns a Triangle.
然后我们将实现一个useFactory()函数,该函数将 type 的函数() => Shape作为参数并返回一个Shape. 我们将尝试传递makeTriangle()给它。
Then we’ll implement a useFactory() function that takes a function of type () => Shape as argument and returns a Shape. We’ll try passing makeTriangle() to it.
声明函数 makeTriangle(): 三角形;
声明函数 makeShape(): 形状;
function useFactory(factory: () => Shape): Shape { 1
return factory(); 1个
}
让 shape1: Shape = useFactory(makeShape); 2
让 shape2: Shape = useFactory(makeTriangle); 2个declare function makeTriangle(): Triangle;
declare function makeShape(): Shape;
function useFactory(factory: () => Shape): Shape { 1
return factory(); 1
}
let shape1: Shape = useFactory(makeShape); 2
let shape2: Shape = useFactory(makeTriangle); 2
这里没有什么不寻常的:我们可以将返回 a 的函数Triangle作为返回 a 的函数传递,Shape因为返回值 (a Triangle) 是 的子类型Shape,因此我们可以将其分配给 a Shape(图 7.5)。
Nothing is out of the ordinary here: we can pass a function that returns a Triangle as a function that returns a Shape because the return value (a Triangle) is a subtype of Shape, so we can assign it to a Shape (figure 7.5).
相反的是行不通的:如果我们改变我们useFactory()期待一个() => Triangle参数并尝试传递它makeShape(),下面的代码将无法编译。
The opposite doesn’t work: if we change our useFactory() to expect a () => Triangle argument and try to pass it makeShape(), the following code won’t compile.
声明函数 makeTriangle(): 三角形;
声明函数 makeShape(): 形状;
function useFactory(factory: () => Triangle ):三角形{ 1
返回工厂();
}
让 shape1: Shape = useFactory(makeShape); 2
让 shape2: Shape = useFactory(makeTriangle);declare function makeTriangle(): Triangle;
declare function makeShape(): Shape;
function useFactory(factory: () => Triangle): Triangle { 1
return factory();
}
let shape1: Shape = useFactory(makeShape); 2
let shape2: Shape = useFactory(makeTriangle);
同样,这段代码非常简单:我们不能使用makeShape()as 类型的函数() => Triangle,因为makeShape()它返回一个Shape对象。该对象可能是 a Triangle,但也可能是 a Square。useFactory()承诺返回 a Triangle,因此它不能返回 的超类型Triangle。当然,它可以返回一个子类型,例如EquilateralTriangle给定一个makeEquilateralTriangle().
Again, this code is pretty straightforward: we can’t use makeShape() as a function of type () => Triangle because makeShape() returns a Shape object. That object could be a Triangle, but it also might be a Square. useFactory() promises to return a Triangle, so it can’t return a supertype of Triangle. It could, of course, return a subtype such as EquilateralTriangle, given a makeEquilateralTriangle().
函数的返回类型是协变的。换句话说,如果Triangle是 的子类型Shape,则函数类型如() => Triangle是 a 的子类型function () => Shape。请注意,函数类型不必描述不带任何参数的函数。如果 和makeTriangle()都makeShape()接受了几个number参数,它们仍然是协变的,正如我们刚刚看到的那样。
Functions are covariant in their return types. In other words, if Triangle is a subtype of Shape, a function type such as () => Triangle is a subtype of a function () => Shape. Note that the function types don’t have to describe functions that don’t take any arguments. If both makeTriangle() and makeShape() took a couple of number arguments, they would still be covariant, as we just saw.
大多数主流编程语言都遵循这种行为。重写继承类型中的方法,更改它们的返回类型,遵循相同的规则。如果我们实现一个ShapeMaker提供make()返回 a 的方法的类Shape,我们可以在派生类中覆盖它MakeTriangle以返回Triangle,如以下清单所示。编译器允许这样做,因为调用任何一个make()方法都会给我们一个Shape对象。
This behavior is followed by most mainstream programming languages. The same rules are followed for overriding methods in inherited types, changing their return type. If we implement a ShapeMaker class that provides a make() method that returns a Shape, we can override it in a derived class MakeTriangle to return Triangle instead, as shown in the following listing. The compiler allows this, as calling either of the make() methods will give us a Shape object.
类 ShapeMaker {
制作():形状{ 1
返回新形状();
}
}
类 TriangleMaker 扩展 ShapeMaker { 2
make(): 三角形 { 3
返回新三角形();
}
}class ShapeMaker {
make(): Shape { 1
return new Shape();
}
}
class TriangleMaker extends ShapeMaker { 2
make(): Triangle { 3
return new Triangle();
}
}
同样,大多数主流编程语言都允许这种行为,因为大多数人认为函数的返回类型是协变的。让我们看看参数类型是彼此的子类型的函数类型会发生什么。
Again, this behavior is allowed in most mainstream programming languages, as most consider functions to be covariant in their return type. Let’s see what happens to function types whose argument types are subtypes of one another.
我们将在这里把事情翻个底朝天,所以我们不会使用一个返回 a 的函数Shape和一个返回 a 的函数Triangle,而是使用一个以 aShape作为参数的函数和一个以 a 作为Triangle参数的函数。我们将调用这些函数drawShape()和drawTriangle()。如何相互关联 (argument: Shape) => void?(argument: Triangle) => void
We’ll turn things inside out here, so instead of using a function that returns a Shape and a function that returns a Triangle, we’ll take a function that takes a Shape as argument and a function that takes a Triangle as argument. We’ll call these functions drawShape() and drawTriangle(). How do (argument: Shape) => void and (argument: Triangle) => void relate to each other?
让我们介绍另一个函数 ,它将 a和一个函数render()作为参数,如下一个清单所示。它只是用给定的调用给定的函数。 Triangle(argument: Triangle) => voidTriangle
Let’s introduce another function, render(), that takes as arguments a Triangle and an (argument: Triangle) => void function, as the next listing shows. It simply calls the given function with the given Triangle.
声明函数 drawShape(shape: Shape): void; 1
声明函数 drawTriangle(triangle: Triangle): void; 1个
函数渲染(
triangle: Triangle, 2
drawFunc: (argument: Triangle) => void): void { 2
drawFunc(triangle); 3
}declare function drawShape(shape: Shape): void; 1
declare function drawTriangle(triangle: Triangle): void; 1
function render(
triangle: Triangle, 2
drawFunc: (argument: Triangle) => void): void { 2
drawFunc(triangle); 3
}
有趣的是:在这种情况下,我们可以安全地传递drawShape()给render()函数!我们可以在需要 (argument: Shape) => voidan 的地方使用 a 。(argument: Triangle) => void
Here comes the interesting bit: in this case, we can safely pass drawShape() to the render() function! We can use a (argument: Shape) => void where an (argument: Triangle) => void is expected.
从逻辑上讲,这是有道理的:我们有一个Triangle,我们将它传递给可以将其用作参数的绘图函数。如果函数本身需要一个Triangle,就像我们的drawTriangle()函数一样,它当然可以工作。但它也应该适用于期望超类型为Triangle. drawShape()想要一个形状——任何形状——来画。因为它不使用任何特定于三角形的东西,所以它比drawTriangle(); 它可以接受任何形状作为参数,无论是 itTriangle还是Square. 所以在这种特殊情况下,子类型关系是相反的。
Logically, it makes sense: we have a Triangle, and we pass it to a drawing function that can use it as an argument. If the function itself expects a Triangle, like our drawTriangle() function, it of course works. But it should also work for a function that expects a supertype of Triangle. drawShape() wants a shape—any shape—to draw. Because it doesn’t use anything that’s triangle-specific, it is more general than drawTriangle(); it can accept any shape as argument, be it Triangle or Square. So in this particular case, the subtyping relationship is reversed.
反转其基础类型的子类型化关系的类型称为逆变。在大多数编程语言中,函数在参数方面是逆变的。期望 as 参数的函数Triangle可以替换期望Shapeas 参数的函数。函数的关系与参数类型的关系相反。如果Triangle是 的子类型Shape,则以 a 作为参数的函数类型是以 a作为参数Triangle的函数类型的超类型(图 7.6)。 Shape
A type that reverses the subtyping relationship of its underlying type is called contravariant. In most programming languages, functions are contravariant with regard to their arguments. A function that expects a Triangle as argument can be substituted for a function that expects a Shape as argument. The relationship of the functions is the reverse of the relationship of the argument types. If Triangle is a subtype of Shape, the type of function that takes a Triangle as an argument is a supertype of the type of function that takes a Shape as an argument (figure 7.6).
我们之前说过“大多数编程语言”。一个值得注意的例外是 TypeScript。在 TypeScript 中,我们也可以做相反的事情:传递一个需要子类型的函数,而不是传递一个需要超类型的函数。这个选择是为了促进常见的 JavaScript 编程模式而做出的明确设计选择。但是,它可能会导致运行时问题。
We said “most programming languages” earlier. A notable exception is TypeScript. In TypeScript, we can also do the opposite: pass a function that expects a subtype instead of a function that expects a supertype. This choice was an explicit design choice made to facilitate common JavaScript programming patterns. It can lead to run-time issues, though.
让我们看一下下一个清单中的示例。首先,我们将isRightAngled()在我们的Triangle类型上定义一个方法,该方法将确定给定实例是否描述直角三角形。方法的实现并不重要。
Let’s look at an example in the next listing. First, we’ll define a method isRightAngled() on our Triangle type, which would determine whether a given instance describes a right-angled triangle. The implementation of the method is not important.
类形状{
/* 形状成员 */
}
声明 const TriangleType:唯一符号;
类三角形扩展形状{
[三角形类型]: void;
isRightAngled(): boolean { 1
让结果: boolean = false;
/* 判断是否为直角三角形 */
返回结果;
}
/* 更多三角形成员 */
}class Shape {
/* Shape members */
}
declare const TriangleType: unique symbol;
class Triangle extends Shape {
[TriangleType]: void;
isRightAngled(): boolean { 1
let result: boolean = false;
/* Determine whether it is a right-angled triangle */
return result;
}
/* More Triangle members */
}
现在让我们反转绘图示例,如代码清单 7.23所示。假设我们的render()函数需要 aShape而不是 aTriangle和可以绘制形状的函数(argument: Shape) => void而不是只能绘制三角形的函数(argument: Triangle) => void。
Now let’s reverse the drawing example, as shown in listing 7.23. Suppose that our render() function expects a Shape instead of a Triangle and a function that can draw shapes (argument: Shape) => void instead of a function that can draw only triangles (argument: Triangle) => void.
声明函数 drawShape(shape: Shape): void; 1
声明函数 drawTriangle(triangle: Triangle): void; 1个
函数渲染(
shape: Shape , 2
drawFunc: (argument: Shape ) => void): void { 2
drawFunc(shape); 3
}declare function drawShape(shape: Shape): void; 1
declare function drawTriangle(triangle: Triangle): void; 1
function render(
shape: Shape, 2
drawFunc: (argument: Shape) => void): void { 2
drawFunc(shape); 3
}
以下是我们如何导致运行时错误:我们可以定义drawTriangle()使用特定于三角形的东西,例如isRightAngled()我们刚刚添加的方法。然后我们用Shape对象(不是Triangle)和调用 render drawTriangle()。
Here’s how we can cause a run-time error: we can define drawTriangle() to use something that is triangle-specific, such as the isRightAngled() method we just added. Then we call render with a Shape object (not a Triangle) and drawTriangle().
NowdrawTriangle()将接收一个对象并尝试在下一个清单中Shape调用它,但是因为不是,这将导致错误。 isRight-Angled()ShapeTriangle
Now drawTriangle() will receive a Shape object and attempt to call isRight-Angled() on it in the next listing, but because the Shape is not a Triangle, this will cause an error.
函数 drawTriangle(三角形:三角形):void {
console.log( triangle.isRightAngled() ); 1个
/* ... */
}
函数渲染(
形状:形状,
drawFunc:(参数:Shape)=> void):void {
drawFunc(形状);
}
渲染(新形状(),drawTriangle); 2个function drawTriangle(triangle: Triangle): void {
console.log(triangle.isRightAngled()); 1
/* ... */
}
function render(
shape: Shape,
drawFunc: (argument: Shape) => void): void {
drawFunc(shape);
}
render(new Shape(), drawTriangle); 2
这段代码可以编译,但它会在运行时失败并出现 JavaScript 错误,因为运行时将无法在我们提供给的对象isRightAngled()上找到。这个结果并不理想,但如前所述,这是在 TypeScript 实现过程中做出的有意识的决定。 ShapedrawTriangle()
This code will compile, but it will fail at run time with a JavaScript error, because the run time won’t be able to find isRightAngled() on the Shape object we gave to drawTriangle(). This result is not ideal, but as mentioned before, it was a conscious decision made during the implementation of TypeScript.
在 TypeScript 中,如果Triangle是 的子类型Shape,则类型的函数(argument: Shape) => void和类型的函数(argument: Triangle) => void可以相互替代。实际上,它们是彼此的子类型。此属性称为双方差。
In TypeScript, if Triangle is a subtype of Shape, a function of type (argument: Shape) => void and a function of type (argument: Triangle) => void can be substituted for each other. Effectively, they are subtypes of each other. This property is called bivariance.
如果从它们的底层类型的子类型化关系来看,它们成为彼此的子类型,则类型是双变的。在 TypeScript 中,如果Triangle是 的子类型Shape,则函数类型(argument: Shape) => void和 (argument: Triangle) => void是彼此的子类型(图 7.7)。
Types are bivariant if, from the subtyping relationship of their underlying types, they become subtypes of each other. In TypeScript, if Triangle is a subtype of Shape, the function types (argument: Shape) => void and (argument: Triangle) => void are subtypes of each other (figure 7.7).
同样,函数在 TypeScript 中关于其参数的双变性允许编译不正确的代码。本书的一个主题是依靠类型系统来消除编译时的运行时错误。在 TypeScript 中,启用通用 JavaScript 编程模式是一项深思熟虑的设计决策。
Again, the bivariance of functions with respect to their arguments in TypeScript allows incorrect code to compile. A major theme of this book is relying on the type system to eliminate run-time errors at compile time. In TypeScript, it was a deliberate design decision to enable common JavaScript programming patterns.
在本节中,我们研究了哪些类型可以替代哪些其他类型。尽管子类型化对于处理简单的继承来说很简单,但是当我们添加在其他类型上参数化的类型时,事情会变得更加复杂。这些类型可以是集合、函数类型或其他泛型类型。这些参数化类型的子类型化关系被移除、保留、反转或根据其底层类型的关系制成双向的方式称为方差:
Throughout this section, we’ve looked at what types can be substituted for what other types. Although subtyping is straightforward for dealing with simple inheritance, things get more complicated when we add types parameterized on other types. These types could be collections, function types, or other generic types. The way that the subtyping relationships of these parameterized types is removed, preserved, reversed, or made two-way based on the relationship of their underlying types is called variance:
尽管编程语言之间存在一些通用规则,但没有一种方法可以支持差异。您应该了解您的编程语言的类型系统的作用以及它如何建立子类型关系。知道这一点很重要,因为这些规则告诉我们什么可以替代什么。您是否需要实现将 a 转换List<Triangle>为 a 的函数List<Shape>,还是可以按List<Triangle>原样使用?答案取决于List<T>您选择的编程语言的差异。
Although some common rules exist across programming languages, there is no one way to support variance. You should understand what the type system of your programming language does and how it establishes subtyping relationships. This is important to know, as these rules tell us what can be substituted for what. Do you need to implement a function to transform a List<Triangle> into a List<Shape>, or can you just use the List<Triangle> as is? The answer depends on the variance of List<T> in your programming language of choice.
在下面的练习中,Triangle是 的子类型Shape。我们将使用 TypeScript 的方差规则。
In the following exercises, Triangle is a subtype of Shape. We are going to use the variance rules of TypeScript.
我们可以将Triangle变量传递给函数吗drawShape(shape: Shape): void?
Can we pass a Triangle variable to a function drawShape(shape: Shape): void?
我们可以将Shape变量传递给函数吗drawTriangle(triangle: Triangle): void?
Can we pass a Shape variable to a function drawTriangle(triangle: Triangle): void?
我们可以将对象数组Triangle( Triangle[]) 传递给函数吗drawShapes(shapes: Shape[]): void?
Can we pass an array of Triangle objects (Triangle[]) to a function drawShapes(shapes: Shape[]): void?
Can we assign the drawShape() function to a variable of function type (triangle: Triangle) => void?
我们可以将drawTriangle()函数分配给函数类型的变量(shape: Shape) => void吗?
Can we assign the drawTriangle() function to a variable of function type (shape: Shape) => void?
我们可以将函数分配getShape(): Shape给函数的变量type () => Triangle吗?
Can we assign a function getShape(): Shape to a variable of function type () => Triangle?
既然我们已经详细介绍了子类型,我们将继续讨论我们没有过多讨论的子类型的一个主要应用:面向对象编程。在第 8 章中,我们将回顾 OOP 的元素及其应用。
Now that we’ve covered subtyping at length, we’ll move on to the one major application of subtyping we haven’t talked about much: object-oriented programming. In chapter 8, we will go over the elements of OOP and their applications.
是的——Painting与 具有相同的形状Wine,但有一个额外的painter属性。在 TypeScript 中,由于结构子类型化,Painting是Wine.
Yes—Painting has the same shape as Wine, with an additional painter property. In TypeScript, due to structural subtyping, Painting is a subtype of Wine.
No—Car缺少定义name的属性Wine,因此即使使用结构子类型,也Car不能替代Wine。
No—Car is missing the name property that Wine defines, so even with structural subtyping, Car cannot be substituted for Wine.
Yes—never是任何其他类型的子类型,包括number,所以我们可以将它分配给一个数字(即使我们永远无法创建实际值,因为makeNothing()永远不会返回)。
Yes—never is a subtype of any other type, including number, so we can assign it to a number (even though we would never be able to create an actual value, as makeNothing() would never return).
No—unknown是任何其他类型的超类型,包括number. 我们可以将 a 分配number给 an unknown,但反之则不行。首先,我们必须确保返回的值makeSomething()是一个数字,然后才能将其分配给x.
No—unknown is a supertype of any other type, including number. We can assign a number to an unknown, but not vice versa. First, we have to ensure that the value returned from makeSomething() is a number before we can assign it to x.
是的——我们可以在需要 Trianglea 的地方替换 a。Shape
Yes—We can substitute a Triangle wherever a Shape is expected.
否——我们不能使用超类型代替子类型。
No—We cannot use a supertype instead of a subtype.
是的——数组是协变的,所以我们可以使用对象数组Triangle而不是Shape对象数组。
Yes—Arrays are covariant, so we can use an array of Triangle objects instead of an array of Shape objects.
是的——函数在 TypeScript 中的参数是双变的,所以我们可以使用(shape: Shape) => voidas (triangle: Triangle) => void。
Yes—Functions are bivariant in their arguments in TypeScript, so we can use (shape: Shape) => void as (triangle: Triangle) => void.
是的——函数在 TypeScript 中的参数是双变的,所以我们可以使用(triangle: Triangle) => voidas (shape: Shape) => void。
Yes—Functions are bivariant in their arguments in TypeScript, so we can use (triangle: Triangle) => void as (shape: Shape) => void.
否——在 TypeScript 中,函数的参数是双变的,但返回类型不是。我们不能将 type 的函数用作 () => Shapetype 的函数() => Triangle。
No—Functions are bivariant in their arguments but not in their return types in TypeScript. We can’t use a function of type () => Shape as a function of type () => Triangle.
本章涵盖
This chapter covers
在本章中,我们将介绍面向对象编程的元素,并了解如何有效地使用它们。您可能熟悉这些概念,因为它们出现在所有面向对象的语言中,因此我们将更多地关注它们的用例。
In this chapter, we will cover the elements of object-oriented programming and see how we can employ them effectively. You are probably familiar with these concepts, as they show up in all object-oriented languages, so we’ll focus more on their use cases.
我们将从接口开始,看看我们如何将它们视为契约。在接口之后,我们将看看继承:我们可以继承数据和行为。继承的替代方法是组合。我们将研究这两种方法之间的一些差异以及何时使用哪种方法。我们将讨论使用混合或 TypeScript 中的交集类型来扩展数据和行为。并非所有语言都支持混入。最后,我们将研究 OOP 的替代方案以及何时不使用它可能有意义。这并不是因为 OOP 有什么问题,而是因为许多开发人员将其作为软件工程的唯一方法来学习,有时它最终会被过度使用。
We’ll start with interfaces and see how we can think of them as contracts. After interfaces, we’ll look at inheritance: we can inherit both data and behavior. An alternative to inheritance is composition. We’ll look at some of the differences between the two approaches and when to use which. We’ll talk about extending data and behavior with mix-ins or, in TypeScript, intersection types. Not all languages support mix-ins. Finally, we’ll look at alternatives to OOP and when it might make sense not to use it. This is not because there is something wrong with OOP, but because many developers learn it as the only approach to software engineering, and sometimes it ends up being overused.
在开始之前,让我们快速定义一下 OOP。
Before getting started, let’s quickly define OOP.
OOP 是一种基于对象概念的编程范式,它同时包含数据和代码。数据是对象的状态。代码是一种或多种方法,也称为消息。在面向对象的系统中,对象可以通过调用彼此的方法来相互“交谈”或发送消息。
OOP is a programming paradigm based on the concept of objects, which contain both data and code. The data is the state of the object. The code is one or more methods, also known as messages. In an object-oriented system, objects can “talk” to or message one another by invoking each other’s methods.
OOP 的两个关键特性是封装,它允许我们隐藏数据和方法,以及继承,它使用额外的数据和/或代码扩展类型。
Two key features of OOP are encapsulation, which allows us to hide data and methods, and inheritance, which extends a type with additional data and/or code.
在本节中,我们将尝试回答一个常见的 OOP 问题:抽象类和接口之间有什么区别?让我们以日志系统为例。我们想提供一种log()方法,但仍然能够使用不同的日志记录实现。我们可以通过几种方式解决这个问题。首先,我们可以声明一个抽象类,ALogger并让实际的实现(例如)Console-Logger从它继承,如以下清单所示。
In this section, we’ll try to answer a common OOP question: what is the difference between an abstract class and an interface? Let’s take as an example a logging system. We want to provide a log() method but still have the ability to use different logging implementations. We can go about this in a couple of ways. First, we can declare an abstract class, ALogger, and have the actual implementations, such as Console-Logger, inherit from it, as shown in the following listing.
抽象类 ALogger { 1
抽象日志(行:字符串):无效; 2个
}
类 ConsoleLogger 扩展 ALogger { 3
日志(行:字符串):void { 3
控制台日志(行);
}
}abstract class ALogger { 1
abstract log(line: string): void; 2
}
class ConsoleLogger extends ALogger { 3
log(line: string): void { 3
console.log(line);
}
}
日志系统的用户会将 anALogger作为参数。我们可以在需要 an 的任何地方传递 的任何子类型ALogger,例如。ConsoleLogerALogger
A user of the logging system would take an ALogger as a parameter. We can pass any subtype of ALogger, such as ConsoleLoger, anywhere that an ALogger is expected.
另一种方法是声明一个ILogger接口并ConsoleLogger实现该接口,如下一个清单所示。
The alternative is to declare an ILogger interface and have ConsoleLogger implement that interface, as shown in the next listing.
接口 ILogger { 1
日志(行:字符串):无效;
}
类 ConsoleLogger 实现 ILogger { 2
log(line: string): void { 2
控制台日志(行);
}
}interface ILogger { 1
log(line: string): void;
}
class ConsoleLogger implements ILogger { 2
log(line: string): void { 2
console.log(line);
}
}
在这种情况下,日志系统的用户将采用 anILogger作为参数。我们可以传递实现接口的任何类型,例如ConsoleLogger,任何ILogger需要 的地方。
A user of the logging system would, in this case, take an ILogger as a parameter. We can pass any type implementing the interface, such as ConsoleLogger, anywhere that an ILogger is expected.
这两种方法很相似,而且都有效,但是在这种情况下,我们应该使用接口,因为接口指定了契约。
The two approaches are similar, and both work, but in a scenario like this one, we should use an interface because an interface specifies a contract.
接口或契约是对一组消息的描述,实现该接口的任何对象都可以理解这些消息。消息是方法,包括名称、参数和返回类型。接口没有任何状态。就像现实世界的合同一样,它是书面协议,接口是实施者将提供的书面协议。
An interface, or a contract, is a description of a set of messages that are understood by any object implementing that interface. The messages are methods and include name, arguments, and return type. An interface does not have any state. Just like real-world contracts, which are written agreements, an interface is a written agreement of what implementers will provide.
log()这正是我们在本例中所需要的:由客户端将调用的方法组成的日志记录合约。声明接口ILogger可以让阅读我们代码的人清楚地知道我们正在指定一个合同。
This is exactly what we need in our case: the logging contract consisting of a log() method that clients will call. Declaring the ILogger interface makes it clear to whoever reads our code that we are specifying a contract.
抽象类可以做到这一点,但它可以做的更多:它可以包含非抽象方法或状态。抽象类与“普通”类或具体类之间的唯一区别是我们不能直接创建抽象类的实例。我们知道,每当我们传递抽象类的实例(例如参数)时ALogger,我们实际上是在处理从 继承的类型的实例ALogger,例如ConsoleLogger。
An abstract class can do that, but it can do much more: it can contain nonabstract methods or state. The only difference between an abstract and a “normal” or concrete class is that we can’t directly create an instance of an abstract class. We know that whenever we pass around an instance of the abstract class, such as an ALogger argument, we are in fact working with an instance of a type that inherits from ALogger, such as ConsoleLogger.
ConsoleLogger这是抽象类和接口之间微妙但重要的区别:和之间的关系ALogger称为is-a 关系,因为它继承自它ConsoleLogger。ALogger另一方面,没有什么可以继承的ILogger,因为它只是指定了一个契约。我们已经ConsoleLogger实现了契约,但它并没有在语义上创建一个is-a关系。满足ConsoleLogger 合同 ILogger但不是. ILogger这就是为什么即使强制一个类只能从另一个类继承的语言(例如 Java 和 C#)仍然允许类实现许多接口的原因。
This is a subtle but important distinction between abstract classes and interfaces: the relationship between ConsoleLogger and ALogger is called an is-a relationship, as in ConsoleLogger is an ALogger, because it inherits from it. On the other hand, there is nothing to inherit from ILogger, as it just specifies a contract. We have ConsoleLogger implement the contract, but it doesn’t semantically create an is-a relationship. ConsoleLogger satisfies the contract ILogger but isn’t an ILogger. That’s the reason why even languages that enforce that a class can inherit from only one other class, such as Java and C#, still allow classes to implement many interfaces.
请注意,我们可以扩展一个接口,基于它创建一个新接口,并使用其他方法。我们可以创建一个向合约IExtendedLogger添加warn()一个方法的方法,例如,如以下清单所示, error()ILogger
Note that we can extend an interface, creating a new interface based on it, with additional methods. We can create an IExtendedLogger that adds a warn() and an error() method to the ILogger contract, for example, as the following listing shows,
接口 ILogger {
日志(行:字符串):无效;
}
接口 IExtendedLogger 扩展 ILogger { 1
警告(行:字符串):无效;
错误(行:字符串):无效;
}interface ILogger {
log(line: string): void;
}
interface IExtendedLogger extends ILogger { 1
warn(line: string): void;
error(line: string): void;
}
满足IExtendedLogger契约的任何对象也自动满足ILogger契约。我们也可以将多个接口合二为一。我们可以以 anISpeaker和 anIVolumeControl为例,定义一个ISpeakerWithVolumeControl结合两者的合约,如代码清单 8.4所示。这种技术允许我们将扬声器功能和音量控制功能用作合同,同时仍然允许其他类型仅实现其中之一。(例如,我们可能对麦克风进行音量控制。)
Any object that satisfies the IExtendedLogger contract also satisfies the ILogger contract automatically. We can also combine multiple interfaces into one. We can take an ISpeaker and an IVolumeControl, for example, and define an ISpeakerWithVolumeControl contract that combines the two, as shown in listing 8.4. This technique allows us to use as a contract both the speaker capabilities and the volume-control capabilities while still allowing other types to implement only one of them. (We might have volume control for a microphone, for example.)
接口 ISpeaker { 1
播放声音(/* ... */):无效;
}
接口 IVolumeControl { 2
音量上升():无效;
音量下降():无效;
}
接口 ISpeakerWithVolumeControl 扩展 ISpeaker, IVolumeControl {
} 3
类 MySpeaker 实现 ISpeakerWithVolumeControl { 4
播放声音(/* ... */):无效{
// 具体实现
}
volumeUp(): void {
// 具体实现
}
音量下降():无效{
// 具体实现
}
}
类音乐播放器{
扬声器:ISpeakerWithVolumeControl; 5个
构造函数(扬声器:ISpeakerWithVolumeControl){
this.speaker = 扬声器;
}
}interface ISpeaker { 1
playSound(/* ... */): void;
}
interface IVolumeControl { 2
volumeUp(): void;
volumeDown(): void;
}
interface ISpeakerWithVolumeControl extends ISpeaker, IVolumeControl {
} 3
class MySpeaker implements ISpeakerWithVolumeControl { 4
playSound(/* ... */): void {
// Concrete implementation
}
volumeUp(): void {
// Concrete implementation
}
volumeDown(): void {
// Concrete implementation
}
}
class MusicPlayer {
speaker: ISpeakerWithVolumeControl; 5
constructor(speaker: ISpeakerWithVolumeControl) {
this.speaker = speaker;
}
}
当然,我们可以MySpeaker同时实现ISpeaker和IVolumeControl而不是,但是使用单一接口可以使组件更容易请求具有音量控制的扬声器。像这样组合界面的能力使我们能够从更小的、可重用的构建块中创建它们。 ISpeakerWithVolumeControlMusicPlayer
We can have MySpeaker implement both ISpeaker and IVolumeControl instead of ISpeakerWithVolumeControl, of course, but using a single interface makes it easier for a component such as MusicPlayer to request a speaker with volume controls. The ability to combine interfaces like this allows us to create them from smaller, reusable building blocks.
接口最终有利于消费者,而不是实现它们的类,所以花一些时间想出最好的设计通常是个好主意。众所周知的针对接口编码的 OOP 原则鼓励使用接口而不是类,正如我们MusicPlayer在示例中所做的那样。该原则减少了系统中组件的耦合,因为我们可以修改甚至换出MySpeaker另一种类型而不会影响MusicPlayer,只要ISpeakerWithVolumeContract满足 即可。
Interfaces ultimately benefit the consumers, not the classes that implement them, so it’s generally a good idea to spend some time coming up with the best design. The well-known OOP principle of coding against interfaces encourages working with interfaces rather than classes, as we did with MusicPlayer in our example. That principle reduces the coupling of the components in the system, as we can modify or even swap out MySpeaker for another type without affecting MusicPlayer, as long as the ISpeakerWithVolumeContract is satisfied.
依赖注入框架负责映射我们应该用于该接口的具体实现,因此其余代码只是请求某个接口,而框架会提供它。这减少了“胶水”代码,使我们能够专注于实现组件本身。我们不会详细介绍依赖注入,但它是减少代码耦合的好方法,对单元测试特别有用,因为我们通常将被测组件的依赖设置为存根或模拟。
Dependency injection frameworks take on the responsibility of mapping the concrete implementation we should use for that interface, so the rest of the code simply asks for a certain interface, and the framework provides it. This reduces the “glue” code and allows us to focus on implementing the components themselves. We won’t cover dependency injection at length, but it’s a good approach to reducing the coupling of the code and especially useful for unit testing, as we usually set up dependencies of components under test to be stubs or mocks.
接下来,我们将研究继承及其一些应用。
Next, we’ll look at inheritance and some of its applications.
getName()函数可以使用具有函数的类型的实例index()。对此建模的最佳方法是什么?
- 声明一个具体的BaseNamed基类
- 声明一个ANamed抽象基类
- 声明一个INamed接口
- 检查getName()运行时是否存在
Instances of types that have a getName() function can be used by an index() function. What is the best way to model this?
- Declare a concrete BaseNamed base class
- Declare an ANamed abstract base class
- Declare an INamed interface
- Check whether getName() exists at run time
在 TypeScript 中,Iterable<T>接口声明了一个[Symbol.iterator]返回 an 的方法Iterator<T>,Iterator<T>接口声明了一个next()返回 an 的方法IteratorResult<T>:
接口可迭代<T> { [Symbol.iterator](): Iterator<T>; } 接口迭代器<T> { 下一个():迭代器结果<T>; }生成器返回这些的组合——一个IterableIterator<T>,它既是可迭代的又是迭代器本身。你将如何定义Iterable-Iterator<T>接口?
In TypeScript, the Iterable<T> interface declares a [Symbol.iterator] method that returns an Iterator<T>, and the Iterator<T> interfaces declares a next() method returning an IteratorResult<T>:
interface Iterable<T> { [Symbol.iterator](): Iterator<T>; } interface Iterator<T> { next(): IteratorResult<T>; }Generators return a combination of these—an IterableIterator<T>, which is both iterable and an iterator itself. How would you define the Iterable-Iterator<T> interface?
继承是面向对象语言最著名的特性之一。它允许我们创建父类的子类。子类继承父类的数据和方法。显然,子类是父类的子类型,因为只要需要父类,就可以始终使用子类的实例。
Inheritance is one of the best-known features of object-oriented languages. It allows us to create subclasses of a parent class. The subclasses inherit both the data and the methods of the parent class. A subclass is, obviously, a subtype of the parent class, as an instance of the subclass can always be used whenever the parent class is expected.
似乎有一个直接的应用程序:如果我们已经有一个实现了我们想要的大部分行为的类,我们可以继承它并添加缺少的东西。随意这样做的问题是双重的。首先,如果我们滥用继承,我们最终会得到非常难以理解和导航的深层类层次结构。其次,我们最终得到一个不一致的数据模型,其中的类没有意义。
There seems to be an immediate application: if we already have a class that implements most of the behavior we want, we can inherit from it and add what is missing. The problem with doing this haphazardly is twofold. First, if we abuse inheritance, we end up with deep hierarchies of classes that are very hard to understand and navigate. Second, we end up with an inconsistent data model in which the classes don’t make sense.
例如,如果我们有一个Point跟踪x和坐标类,我们可以从它继承一个并添加一个属性。我们可以通过圆心和半径来定义一个圆,并且已经可以表示圆心了。但这个定义应该让人觉得奇怪。 yCircleradiusPoint
If we have a Point class that tracks x and y coordinates, for example, we could inherit a Circle from it and add a radius property. We can define a circle by its center and radius, and Point can already represent the center. But this definition should feel odd.
类点{
x:数字;
y:数字;
构造函数(x:数字,y:数字){
这个.x = x;
这个.y = y;
}
}
类圆扩展点{ 1
半径:数字;
构造函数(x:数字,y:数字,半径:数字){
超级(x,y);
this.radius = 半径;
}
}class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class Circle extends Point { 1
radius: number;
constructor(x: number, y: number, radius: number) {
super(x, y);
this.radius = radius;
}
}
要理解为什么这感觉很奇怪,让我们看看我们建立的is-a关系。子类的实例在逻辑上是超类的实例吗?在这种情况下,不。一个 Circle不是一个Point。我们当然可以按照我们定义它的方式将其作为一个整体来使用,但似乎没有一个我们想要这样做的合理场景。
To understand why this feels odd, let’s look at the is-a relationship we established. Is an instance of the subclass logically an instance of the superclass? In this case, no. A Circle is not a Point. We can certainly use it as one, the way we defined it, but there doesn’t seem to be a reasonable scenario in which we would want to do that.
继承在子类型与其父类型之间建立is-a关系。如果我们的基类是Shape,而我们的派生类是Circle,则关系是“Circle是一个Shape”。这是继承的语义,也是一个很好的测试,可以应用于两种类型,以确定我们是否应该使用继承。
Inheritance establishes an is-a relationship between the child type and its parent type. If our base class is Shape, and our derived class is Circle, the relationship is “Circle is a Shape.” This is the semantic meaning of inheritance and a good test to apply to two types to determine whether we should use inheritance.
我们将在 8.3 节中讨论另一种组合方法。在那之前,让我们看一下使用继承确实有意义的 几种情况。
We’ll go over the alternative approach of composition in section 8.3. Until then, let’s look at a few situations in which it does make sense to use inheritance.
我们应该考虑继承的一个例子是我们的数据模型是分层的。这个事实相当明显,所以我们不会详细介绍,但这是继承的最佳用途:当我们沿着继承链向下移动时,我们通过添加更多数据和/或更多行为来改进我们的类型(图8.1) .
One instance when we should look at inheritance is when our data model is hierarchical. This fact is fairly obvious, so we won’t cover it at length, but this is the best use of inheritance: as we move down the inheritance chain, we refine our types by adding more data and/or more behavior (figure 8.1).
图中的例子看似简单,却是对继承的完美运用。A Catis a Petis an Animal,随着我们深入层次结构,我们会得到更多的行为和状态。
The example in the figure may seem to be simplistic, but it is a perfect use of inheritance. A Cat is a Pet is an Animal, and as we go deeper down the hierarchy, we get more behavior and state.
当我们想要处理更高的抽象级别时,我们会在层次结构中往上走。如果我们只需要play()我们的动物,我们使用类型的参数Pet。如果我们需要特定的喵喵叫行为,我们使用类型的参数Cat。
When we want to deal with a higher abstraction level, we go up the hierarchy. If we just need to play() with our animal, we use an argument of type Pet. If we need specific meowing behavior, we use an argument of type Cat.
这个例子应该非常简单,所以让我们继续一个更有趣的继承应用,它有一个转折点:不同的派生类以不同的方式实现某些行为。
This example should be very straightforward, so let’s move on to a more interesting application of inheritance, which has a twist: different derived classes implement some behavior differently.
我们应该使用继承的另一种情况是,当我们想要的大部分行为和状态对多种类型都是通用的,但其中一小部分需要在不同的实现中有所不同时。多种类型仍应通过我们的is-a测试。
The other situation in which we should use inheritance is when most of the behavior and state we want is common to multiple types, but a small part of it needs to vary across implementations. The multiple types should still pass our is-a test.
我们有一个可以计算为数字的表达式,我们有有两个操作数的二进制表达式,我们有求和和乘法表达式,我们通过对操作数进行加法和乘法来求值。
We have an expression that can be evaluated to a number, we have binary expressions that have two operands, and we have sum and multiply expressions that we evaluate by adding and multiplying the operands.
我们可以将表达式建模为IExpression带有eval()方法的接口。我们使它成为一个接口,因为它不持有任何状态。接下来,我们实现一个存储两个操作数的抽象类,如清单 8.6BinaryExpression所示,但我们保持抽象并让派生类实现它。每个都继承了两个操作数并提供了自己的实现(图 8.2)。 eval()Sum-ExpressionMulExpressionBinary-Expressioneval()
We can model an expression as an IExpression interface with an eval() method. We make it an interface because it doesn’t hold any state. Next, we implement a BinaryExpression abstract class that stores the two operands, as shown in listing 8.6, but we keep eval() abstract and let derived classes implement it. Sum-Expression and MulExpression each inherit the two operands from Binary-Expression and provide their own eval() implementation (figure 8.2).
接口 IExpression { 1
评估():数字;
}
抽象类 BinaryExpression 实现 IExpression { 2
只读一个:数字;
只读 b:数字;
构造函数(a:数字,b:数字){
这个.a = a;
这个.b = b;
}
抽象评估():数字; 3个
}
类 SumExpression 扩展 BinaryExpression { 4
评估():数字{
返回 this.a + this.b;
}
}
类 MulExpression 扩展 BinaryExpression { 4
评估():数字{
返回this.a * this.b;
}
}interface IExpression { 1
eval(): number;
}
abstract class BinaryExpression implements IExpression { 2
readonly a: number;
readonly b: number;
constructor(a: number, b: number) {
this.a = a;
this.b = b;
}
abstract eval(): number; 3
}
class SumExpression extends BinaryExpression { 4
eval(): number {
return this.a + this.b;
}
}
class MulExpression extends BinaryExpression { 4
eval(): number {
return this.a * this.b;
}
}
这应该通过我们的is-a测试:a SumExpressionis a BinaryExpression。当我们沿着层次结构往下走时,我们继承了公共部分(在我们的例子中,两个操作数)但是eval()为每个派生类参数化。
This should pass our is-a test: a SumExpression is a BinaryExpression. As we go down the hierarchy, we inherit the common parts (in our case, the two operands) but parameterize the eval() for each derived class.
需要注意的一件事是提出非常深的类层次结构,这使得代码更难导航,因为状态的各个部分和对象的方法来自层次结构中的不同级别。
One thing to watch out for is coming up with very deep hierarchies of classes, which makes the code harder to navigate, as various parts of the state and methods of an object come from different levels in the hierarchy.
通常,让子类成为具体的类而让层次结构中的所有父类都是抽象的也很好。这种技术可以更轻松地跟踪事物并避免意外行为。当子类重写父方法时可能会发生意外行为,但随后我们将其向上转换并将其作为父类型传递。这样一个对象的行为与父类的实例不同,这对于代码的维护者来说可能不直观。
Usually, it’s also good to have the children be concrete classes and all parents up the hierarchy be abstract. This technique makes it easier to keep track of things and avoid unexpected behavior. Unexpected behavior can happen when a child class overrides a parent method, but then we upcast it and pass it around as the parent type. Such an object would behave differently from an instance of the parent class, which might not be intuitive for maintainers of the code.
一些语言提供了一种方法来显式地将子类标记为不可继承的,以强制停止那里的层次结构。通常,这是使用诸如finalor之类的关键字来完成的sealed。我们应该尽可能多地使用它们。如果我们想覆盖或扩展行为,我们有一个更好的继承替代方案:组合。
Some languages provide a way to explicitly mark a child class as noninheritable to enforce stopping the hierarchy there. Usually, this is done with keywords such as final or sealed. We should use these as often as we can. If we want to override or extend behavior, we have a better alternative to inheritance: composition.
以下哪项看起来像是对继承的良好使用?
- File延伸Folder。
- Triangle延伸Point。
- Parser延伸Compiler。
- 以上都不是。
Which of the following looks like a good use of inheritance?
- File extends Folder.
- Triangle extends Point.
- Parser extends Compiler.
- None of the above.
UnaryExpression使用具有单个操作数的 和UnaryMinusExpression切换其操作数符号的扩展本节中的示例。(例如,示例 1 变为 –1,而 –2 变为 2。)
Extend the example in this section with a UnaryExpression that has a single operand and a UnaryMinusExpression that toggles the sign of its operand. (Example 1 becomes –1, for example, and –2 becomes 2.)
面向对象编程的一个众所周知的原则是尽可能优先使用组合而不是继承。让我们看看组合是关于什么的。
A well-known principle of object-oriented programming is to prefer composition over inheritance whenever possible. Let’s see what composition is about.
回到我们Point的Circle例子,我们可以创建一个Circle的孩子Point,但这不太正确。让我们扩展我们的示例并Shape在清单 8.7中引入一个。我们会说我们系统中的所有形状都需要有一个标识符,因此Shape有一个idtype 属性string。ACircle是一个Shape,所以我们可以继承id。另一方面,Circle 有一个中心,所以它将包含一个center类型为 的属性Point。
Going back to our Point and Circle example, we can make a Circle a child of Point, but that wouldn’t be quite right. Let’s expand our example and introduce a Shape in listing 8.7. We’ll say that all shapes in our system need to have an identifier, so Shape has an id property of type string. A Circle is a Shape, so we can inherit the id. On the other hand, the Circle has a center, so it will contain a center property of type Point.
类形状{
编号:字符串;
构造函数(id:字符串){
这个.id = id;
}
}
类点{
x:数字;
y:数字;
构造函数(x:数字,y:数字){
这个.x = x;
这个.y = y;
}
}
类 Circle extends Shape { 1
center: Point; 2个
半径:数字;
构造函数(id:字符串,中心:点,半径:数字){
超级(身份证);
this.center = 中心;
this.radius = 半径;
}
}class Shape {
id: string;
constructor(id: string) {
this.id = id;
}
}
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class Circle extends Shape { 1
center: Point; 2
radius: number;
constructor(id: string, center: Point, radius: number) {
super(id);
this.center = center;
this.radius = radius;
}
}
就像我们应用的is-a测试来确定我们是否应该Circle继承自一样Point,我们可以对组合应用类似的测试:has-a(图 8.3)。
Just like the is-a test we applied to determine whether we should have Circle inherit from Point, we can apply a similar test for composition: has-a (figure 8.3).
我们可以定义该类型的属性,而不是从类型继承行为。这种技术仍然为我们提供包含类型存储的状态,但作为我们类型的组成部分而不是我们类型的继承部分。
Instead of inheriting behavior from a type, we can define a property of that type. This technique still gives us the state that the contained type stores but as a component part of our type rather than an inherited part of our type.
组合在容器类型和包含的类型之间建立了has-a关系。如果我们的类型是Circle,而我们包含的类是Point,则关系是“Circle有一个Point”(它定义了它的中心)。这是组合的语义,也是一个很好的测试,可以应用于两种类型,以确定我们是否应该使用组合。
Composition establishes a has-a relationship between a container type and the contained type. If our type is Circle, and our contained class is Point, the relationship is “Circle has a Point” (which defines its center). This is the semantic meaning of composition and a good test to apply to two types to determine whether we should use composition.
center组合的一个主要好处是来自组件属性的所有状态(例如a 的坐标Circle)都封装在这些组件中(例如centertype 的属性Point),因此我们的类型更清晰。
A major benefit of composition is that all the state coming from component properties (such as the coordinates of the center of a Circle) is encapsulated in those components (such as the center property of type Point), so our type is much cleaner.
circle我们类型的一个实例Circle有一个circle.id属性,它继承自Shape,但其中心点的x和中心坐标在:和 中。如果我们愿意,我们可以将其设为私有,在这种情况下,外部代码将无法访问它。我们不能用继承的属性来做到这一点:如果声明为公共的,就不能隐藏它。 ycentercircle.center.xcircle.center.ycenterShapeidCircle
An instance circle of our Circle type has a circle.id property, which it inherits from Shape, but the x and y center coordinates from its center point are in center: circle.center.x and circle.center.y. If we want, we can make center private, and in that case external code wouldn’t be able to access it. We cannot do that with an inherited property: if Shape declares id as public, Circle cannot hide it.
接下来我们将讨论组合的一些应用,但一般来说,这种方法是使状态和行为对类可用的首选方法,而不是继承它。除非两种类型之间 存在明确的is-a关系,否则组合是一个很好的默认值。
We’ll go over a few applications of composition next, but in general, this method is the preferred way of making state and behavior available to a class, as opposed to inheriting it. Unless there is a clear is-a relationship between two types, composition is a good default.
我们将从另一个简单、直接的示例开始,因为这也是您可能熟悉的概念。它出现在面向对象编程中(以及它之外)的任何地方。
We’ll start with another simple, straightforward example because again, this is a concept you are likely familiar with. It shows up everywhere in object-oriented programming (and outside it).
一家公司有很多组成部分:各个部门、运营预算、首席执行官等等。所有这些部分都是Company. 我们在第 3 章讨论产品类型时介绍了此类类型的一个方面。如果我们暂时只看一家公司可能处于的状态集,它是每个部门所处状态、预算所处状态、首席执行官所处状态等的产物。额外的变化是我们可以通过将其设为私有来封装该状态的一部分,并使用可以在其实现中访问私有的其他方法来增强复合类(这是外部函数无法做到的)。
A company has many constituent parts: various departments, an operating budget, a CEO, and so on. All these parts are properties of Company. We covered an aspect of such types in chapter 3, when we talked about product types. If, for a moment, we simply look at the set of possible states a company can be in, it’s the product of the state each department is in, the state the budget is in, the state the CEO is in, and so on. The additional twist is that we can encapsulate parts of this state by making it private and enhance the composite class with additional methods that can access the privates in their implementation (something that an external function wouldn’t be able to do).
例如,我们不能简单地找到公司的首席执行官并问他们一个问题。我们可以尝试通过官方渠道联系公司,向 CEO 发送消息,CEO 可能会也可能不会回复我们,如下列表所示。
We can’t simply get the CEO of the company and ask them a question, for example. We can try sending a message to the CEO by contacting the company through official channels, and the CEO might or might not get back to us, as the next listing shows.
班级CEO { 1
isBusy(): 布尔值 {
/* ... */
}
答案(问题:字符串):字符串{
/* ... */
}
}
类部门{
/* ... */
}
类预算{
/* ... */
}
公司类 { 2
私人CEO:CEO = new CEO();
私人部门:Department[] = [];
私人预算:Budget = new Budget();
问CEO(问题:字符串):字符串| undefined { 3
if (!this.ceo.isBusy()) { 4
return this.ceo.answer(question); 4个
}
}
}class CEO { 1
isBusy(): boolean {
/* ... */
}
answer(question: string): string {
/* ... */
}
}
class Department {
/* ... */
}
class Budget {
/* ... */
}
class Company { 2
private ceo: CEO = new CEO();
private departments: Department[] = [];
private budget: Budget = new Budget();
askCEO(question: string): string | undefined { 3
if (!this.ceo.isBusy()) { 4
return this.ceo.answer(question); 4
}
}
}
与普通的旧产品类型(如元组和记录)相比,隐藏类成员并提供对它们的受控访问的能力是封装给表带来的关键额外区别之一。
The ability to hide class members and provide controlled access to them is one of the key extra distinctions that encapsulation brings to the table compared with plain old product types such as tuples and records.
您可能还听说过值类型和引用类型,或者结构类型和类类型之间的区别等等。尽管那里有很多细微差别,但不幸的是,其中很少有足够的通用性。不同的编程语言以不同的方式实现这些类型,因此更多的是了解您的语言如何处理细微差别。
You might also have heard of value types and reference types, or about differences between struct and class types and so on. Although there is a lot of nuance to cover there, unfortunately, little of it is general enough. Different programming languages implement these types differently, so it’s more a matter of understanding how your language handles the nuances.
通常,当我们将值类型的实例分配给变量或将其作为参数传递给函数时,其内容会被复制到内存中,从而有效地创建一个不同的实例。另一方面,当我们分配一个引用类型的实例时,完整的状态不会被复制——只是对它的引用。旧变量和新变量都指向同一个对象并且可以改变它的状态。
In general, when we assign an instance of a value type to a variable or pass it as an argument to a function, its content gets copied in memory, effectively creating a distinct instance. On the other hand, when we assign an instance of a reference type, the full state doesn’t get copied—just a reference to it. Both the old and new variables point to the same object and can alter its state.
我们在这里没有深入讨论这个主题的原因是,由于每种语言实现这些概念的方式不同,它可能会变得非常混乱。例如,在 C# 中,结构看起来很像类,但它是值类型;分配它会导致其状态被复制。另一方面,Java 不支持开箱即用的原始数值类型之外的适当值类型:一切都是引用类型。C++ 又是不同的:C++ 中的结构简单地意味着成员在默认情况下是公共的,在类中默认是私有的。在 C++ 中,一切都是值,除非我们显式地将值声明为指针 (*) 或引用 (&)。一些函数式语言使用不可变数据,其中不存在值和引用之间的区别,
The reason why we are not covering this topic in depth here is that it might get very confusing because of the way each language implements these concepts. In C#, for example, a struct looks a lot like a class, but it is a value type; assigning it causes its state to be copied. On the other hand, Java does not support proper value types outside the primitive numerical types that come out of the box: everything is a reference type. C++, again, is different: a struct in C++ simply means that members are public by default and private by default in classes. In C++, everything is by value, unless we explicitly declare a value as pointer (*) or reference (&). Some functional languages work with immutable data, in which the distinction between value and reference doesn’t exist, as everything is moved around.
尽管值类型和引用类型之间的区别很重要(我们不想复制大量数据,因为它会影响性能;我们宁愿复制也不愿共享,因为只有一个状态所有者更安全),你应该明白您的编程语言如何表达和处理这些细微差别。
Although the difference between value and reference types matters (we don’t want to copy large amounts of data, as it affects performance; we’d rather copy than share because it’s safer to have a single owner of the state), you should understand how your programming language expresses and handles these nuances.
接下来,让我们看看另一个可能不太明显的组合应用:非常有用的适配器模式。
Next, let’s look at another, maybe not-so-obvious application of composition: the very useful adapter pattern.
适配器模式可以使两个类兼容,而不需要我们修改两个类中的任何一个。适配器的使用非常类似于物理适配器。我们可能有一台只有 USB 端口的笔记本电脑,并且想将它连接到有线网络,例如,我们会使用以太网电缆来实现。以太网到 USB 适配器管理两个不兼容组件(USB 和以太网)之间的转换,并确保它们协同工作。
The adapter pattern can make two classes compatible without requiring us to modify either of the two classes. An adapter is used very much like a physical adapter. We might have a laptop with only USB ports and want to connect it to a wired network, for example, which we would do with an Ethernet cable. An Ethernet-to-USB adapter manages the translation between the two incompatible components, USB and Ethernet, and ensures that they work together.
例如,假设我们使用一个外部几何库,它提供了一些我们需要的重要操作,但它不适合我们的对象模型。它期望根据ICircle接口定义一个圆,该接口声明两个方法来获取圆心的 x 和 y 坐标,getCenterX()以及getCenterY()另一个方法 ,getDiameter()来获取圆的直径,如以下代码所示。
As an example, let’s say we use an external geometry library that provides some important operations we need, but it doesn’t fit our object model. It expects a circle to be defined in terms of an ICircle interface that declares two methods to get the x and y coordinates of the center, getCenterX() and getCenterY(), and another method, getDiameter(), to get the diameter of the circle, as shown in the following code.
命名空间几何库 {
导出接口 ICircle { 1
getCenterX(): 数字;
getCenterY(): 数字;
getDiameter():数字;
}
/* 省略对 ICircle 的操作 */ 2
}namespace GeometryLibrary {
export interface ICircle { 1
getCenterX(): number;
getCenterY(): number;
getDiameter(): number;
}
/* Operations on ICircle omitted */ 2
}
我们Circle是根据中心定义的Point和半径定义。假设我们有一个很大的代码库,而这个圈子只是其中的一小部分,我们可能不希望 重构一切只是为了与这个库兼容。好消息是有一个更简单的解决方案:我们可以实现一个CircleAdapter类,它包装一个,实现预期的接口,并处理从 our到库预期的 Circle转换逻辑。Circle
Our Circle is defined in terms of a center Point and a radius. Assuming that we have a large codebase, and this circle is just a small piece of it, we probably don’t want to refactor everything just to be compatible with this library. The good news is that there is an easier solution: we can implement a CircleAdapter class, which wraps a Circle, implements the expected interface, and handles the logic of converting from our Circle to what the library expects.
类 CircleAdapter 实现 GeometryLibrary.ICircle { 1 个
私人圈子:Circle; 2个
构造函数(圆:圆){
this.circle = 圆圈
}
getCenterX(): 数字 {
返回 this.circle.center.x; 3个
}
getCenterY(): 数字 {
返回 this.circle.center.y; 3个
}
getDiameter(): 数字 {
返回 this.circle.radius * 2; 4个
}
}class CircleAdapter implements GeometryLibrary.ICircle { 1
private circle: Circle; 2
constructor(circle: Circle) {
this.circle = circle
}
getCenterX(): number {
return this.circle.center.x; 3
}
getCenterY(): number {
return this.circle.center.y; 3
}
getDiameter(): number {
return this.circle.radius * 2; 4
}
}
现在,每当我们需要将几何库与Circle实例一起使用时,我们都会CircleAdapter为它创建一个并将其传递给几何库。适配器模式对于处理我们无法修改的代码非常有用,例如来自我们无法控制的外部库的代码。事实上,这就是适配器模式的一般结构,如图8.4所示。
Now, whenever we need to use the geometry library with a Circle instance, we create a CircleAdapter for it and pass that to the geometry library. The adapter pattern is extremely useful for dealing with code that we cannot modify, such as code that comes from external libraries outside our control. This is, in fact, the general structure of the adapter pattern as shown in figure 8.4.
适配器可以通过将其标记为私有来隐藏它转换的实际实现。这是一个有趣的组合应用:我们不是将多个组件组合在一起,而是包装一个组件,但提供需要作为另一种类型使用的“胶水”。
The adapter can hide the actual implementation it translates from by marking it as private. This is an interesting application of composition: instead of bringing together several components, we wrap a single component but provide the “glue” it needs to be consumed as another type.
通过接口、继承和组合,我们已经涵盖了面向对象编程的最常见元素。接下来,我们将看看一个稍微更高级(也更有争议!)的概念:mix-ins。
With interfaces, inheritance, and composition out of the way, we’ve covered the most common elements of object-oriented programming. Next, we’ll look at a slightly more advanced (and more controversial!) concept: mix-ins.
您将如何为FileTransfer使用 aConnection通过网络传输文件的类建模?
- FileTransfer扩展Connection(从Connection类型继承连接行为)。
- FileTransferimplements IConnection(实现一个声明连接行为的接口)。
- FileTransfer包装一个Connection(类成员提供连接功能)。
- Connectionextends abstract FileTransfer(连接扩展抽象 FileTransfer 类并提供所需的额外行为)。
How would you model a FileTransfer class that uses a Connection to transfer files over the network?
- FileTransfer extends Connection (inherits connection behavior from the Connection type).
- FileTransfer implements IConnection (implements an interface that declares connection behavior).
- FileTransfer wraps a Connection (class member provides connection functionality).
- Connection extends abstract FileTransfer (connection extends the abstract FileTransfer class and provides the additional behavior required).
给定一个类,实现一个Airplane有两个机翼和每个机翼一个引擎的飞机。Engine尝试使用合成来对此建模。
Implement an Airplane with two wings and an engine on each wing, given an Engine class. Try to model this by using composition.
另一种为类型引入额外数据或行为的方法并不完全是继承,但不幸的是,它主要在支持它的语言中实现。
Another way to bring in additional data or behavior to a type is not quite inheritance, though unfortunately, it is mostly implemented as such in the languages that support it.
让我们回到我们最简单的动物示例:a Catis a Petis an Animal。让我们WildAnimal在我们的层次结构中引入一个类型和Wolf它的一个子类型。野生动物可以roam(),狼也可以打猎。狩猎由三种不同的方法组成:track()、stalk()和pounce()(图 8.5)。
Let’s go back to our simplistic animal example: a Cat is a Pet is an Animal. Let’s introduce a WildAnimal type in our hierarchy and a Wolf child type of that. Wild animals can roam(), and a wolf can also hunt. Hunting consists of three separate methods: track(), stalk(), and pounce() (figure 8.5).
如果需要,我们甚至可以IHunter使用标准track()、stalk()和pounce()方法实现一个接口。
If we want, we can even implement an IHunter interface with the standard track(), stalk(), and pounce() methods.
如果我们Tiger在组合中添加一个类型会怎样?ATiger也可以打猎,假设捕食者的狩猎行为相似,我们不想在我们的Wolf和Tiger类型中重复代码。一种选择是在层次结构:一种类型,它是andHunter的子代WildAnimal和父代(图 8.6)。 WolfTiger
What if we add a Tiger type to the mix? A Tiger can also hunt, and assuming that hunting behavior is similar across predators, we don’t want to duplicate the code across our Wolf and Tiger types. One option is to introduce a common type in the hierarchy: a Hunter type, which is the child of WildAnimal and the parent of Wolf and Tiger (figure 8.6).
这种方法一直有效,直到我们意识到 aCat也在狩猎。我们如何在不完全改变我们的类型层次结构的情况下使所有这些猎人行为可用Cat?
This approach works until we realize that a Cat also hunts. How do we make all this hunter behavior available to Cat without completely rejiggering our type hierarchy?
一种方法是定义一个IHunter接口和一个类来封装常见的狩猎行为,如清单 8.11HuntingBehavior所示。然后我们可以让所有三种类型Cat—— 、Wolf和Tiger——包装一个Hunting-Behavior实例并将接口的实现转发给它(图 8.7)。
One way to go about it is to define an IHunter interface and a HuntingBehavior class that encapsulates the common hunting behavior, as shown in listing 8.11. Then we can have all three of our types—Cat, Wolf, and Tiger—wrap a Hunting-Behavior instance and forward the implementation of the interface to it (figure 8.7).
接口 IHunter { 1
跟踪():无效;
茎():无效;
突袭():无效;
}
类 HuntingBehavior 实现 IHunter { 2
祈祷:动物 | 不明确的;
跟踪():无效{
/* ... */
}
茎():无效{
/* ... */
}
突袭():无效{
/* ... */
}
}
Cat 类扩展 Pet 实现 IHunter {
私人狩猎行为:狩猎行为=新的狩猎行为(); 3个
跟踪():无效{
这个.huntingBehavior.track(); 4个
}
茎():无效{
这个.huntingBehavior.track(); 4个
}
突袭():无效{
这个.huntingBehavior.track(); 4个
}
喵():无效{
/* ... */
}
}interface IHunter { 1
track(): void;
stalk(): void;
pounce(): void;
}
class HuntingBehavior implements IHunter { 2
pray: Animal | undefined;
track(): void {
/* ... */
}
stalk(): void {
/* ... */
}
pounce(): void {
/* ... */
}
}
class Cat extends Pet implements IHunter {
private huntingBehavior: HuntingBehavior = new HuntingBehavior(); 3
track(): void {
this.huntingBehavior.track(); 4
}
stalk(): void {
this.huntingBehavior.track(); 4
}
pounce(): void {
this.huntingBehavior.track(); 4
}
meow(): void {
/* ... */
}
}
这种方法可行,但我们最终得到了几个IHunter通过包装实现的类HuntingBehavior。现在,将新的狩猎动物添加到我们的层次结构中会附带一堆样板,我们必须从另一种类型复制/粘贴这些样板。更糟糕的是,界面的添加IHunter会导致我们的代码库发生级联变化,因为我们必须更新每只动物的狩猎行为,即使真正改变的只是它本身HuntingBehavior。
This approach works, but we end up with several classes that implement IHunter by wrapping HuntingBehavior. Adding a new hunting animal to our hierarchy now comes with a bunch of boilerplate that we have to copy/paste from another type. Even worse, an addition to the IHunter interface causes a cascade of changes in our code base, as we have to update each individual animal with hunting behavior, even though the only thing that really changes is the HuntingBehavior itself.
有没有更好的方法来实现这个?答案是肯定的,也不是。
Is there a better way of implementing this? The answer is both yes and no.
让所有狩猎动物都具有这种行为的一种更简单的方法是将其混合到每种类型中。不幸的是,混合行为的方式通常是通过多重继承来实现的。这个事实是不幸的,因为它与我们在本章开头介绍的is-a经验法则不一致。我们甚至还没有涵盖多重继承的所有风险(我们也不会,但如果您好奇的话,请查看菱形继承问题)。
An easier way to have all hunting animals share this behavior is to mix it into each type. Unfortunately, the way to mix in behavior is usually achieved with multiple inheritance. This fact is unfortunate because it is at odds with what we covered at the beginning of the chapter with the is-a rule of thumb. We haven’t even covered all the perils of multiple inheritance (and we won’t, but look up the diamond inheritance problem if you are curious).
我们可以从多重继承的角度来看,创建一个Hunter实现狩猎行为的类,所有狩猎动物都派生自它。那么 aCat既是 anAnimal又是 a Hunter。
We can look at this from the multiple-inheritance point of view, creating a Hunter class that implements the hunting behavior and have all hunting animals derive from it. Then a Cat is both an Animal and a Hunter.
另一方面,混合与继承不同。我们可以创建一个Hunter-Behavior实现狩猎行为的类,让所有狩猎动物都包含这种行为。
On the other hand, mix-ins aren’t the same as inheritance. We can create a Hunter-Behavior class that implements the hunting behavior and have all hunting animals include this behavior.
混入在类型与其混入类型之间建立包含关系。如果我们的班级是Cat,而我们的混入班级是HunterBehavior,则关系是“Cat包括Hunter-Behavior” 这是mix-ins的语义,不同于is-a继承关系(图8.8)。
Mix-ins establish an includes relationship between a type and its mixed-in type. If our class is Cat, and our mixed-in class is HunterBehavior, the relationship is “Cat includes Hunter-Behavior.” This is the semantic meaning of mix-ins and is different from the is-a relationship of inheritance (figure 8.8).
mix-in 微妙且有争议的原因是许多语言并不完全支持它们以使事情简单,并且在大多数支持它们的语言中,混合另一种类型与继承没有区别。这是有道理的,因为在我们混合了一个类之后HunterBehavior,我们的Cat类自动成为该类的子类型。Cat我们可以在任何需要的时候传入一个实例HunterBehavior,但是is-a测试失败了:a Catis not HunterBehavior。
The reason why mix-ins are nuanced and controversial is that many languages don’t support them altogether to keep things simple, and in most languages that do support them, mixing in another type is indistinguishable from inheritance. This makes sense, as after we mix in a class such as HunterBehavior, our Cat class automatically becomes a subtype of that class. We can pass in a Cat instance whenever we need HunterBehavior., but the is-a test fails: a Cat is not HunterBehavior.
混合对于减少样板代码非常有用。它们允许我们通过混合不同的行为来组合一个对象,并在多种类型之间重用共同的行为。它们最适合用于实现横切关注点:影响其他关注点且不易分解的程序方面。想想引用计数、缓存、持久性等。
Mix-ins are very useful for reducing boilerplate code. They allow us to put together an object by mixing in different behaviors and to reuse common behavior across multiple types. They are best used to implement cross-cutting concerns: aspects of a program that affect other concerns and can’t be easily decomposed. Think of things like reference counting, caching, persistence, and so on.
我们将快速浏览一个 TypeScript 示例,但语法是非常特定于该语言的。如果它看起来很复杂,请不要担心;基本原则是重要的部分。
We’ll quickly go over a TypeScript example, but the syntax is very specific to the language. Don’t worry if it looks complicated; the underlying principle is the important part.
混合两种类型的一种方法是使用一个extend()函数,该函数接受两种不同类型的两个实例,并将第二个实例的所有成员复制到第一个实例中,如清单8.12所示。由于底层 JavaScript 语言的动态特性,我们可以在 TypeScript 中执行此操作。在 JavaScript 中,我们可以在运行时添加和删除对象的成员。extend()是通用的,因此它可以处理任何两种类型的实例。
One way to mix two types is to use an extend() function that takes two instances of two different types and copies all members of the second instance to the first one, as shown in listing 8.12. We can do this in TypeScript because of the dynamic nature of the underlying JavaScript language. In JavaScript, we can add and remove members of an object at run time. extend() is generic, so it can work with instances of any two types.
函数扩展<第一,第二>(第一:第一,第二:第二):
第一和第二 { 1
常量结果:未知={};
for (const prop in first) { 2
如果 (first.hasOwnProperty(prop)) {
(<第一个>结果)[prop] = first[prop];
}
}
for (const prop in second) { 3
如果 (second.hasOwnProperty(prop)) {
(<第二>结果)[prop] = second[prop];
}
}
返回<第一和第二>结果;
}function extend<First, Second>(first: First, second: Second):
First & Second { 1
const result: unknown = {};
for (const prop in first) { 2
if (first.hasOwnProperty(prop)) {
(<First>result)[prop] = first[prop];
}
}
for (const prop in second) { 3
if (second.hasOwnProperty(prop)) {
(<Second>result)[prop] = second[prop];
}
}
return <First & Second>result;
}
这是我们第一次遇到&语法:First & Second定义一个类型,它具有 的所有成员First和 的所有成员Second。这在 TypeScript 中称为交集类型。不要太担心这个特定的实现;重要的是将两种类型组合成包含其两个成员的类型的概念。
This is the first time we encounter the & syntax: First & Second defines a type that has all the members of First and all the members of Second. This is called an intersection type in TypeScript. Don’t worry too much about this particular implementation; what’s important is the concept of combining two types into a type that contains both their members.
大多数语言不会让在运行时向对象添加新成员变得如此容易,但在 JavaScript 中是可能的——因此,在 TypeScript 中也是如此。作为编译时的替代方案,在 C++ 中,我们可以使用多重继承将一个类型声明为两个其他类型的组合。
Most languages don’t make it so easy to add new members to an object at run time, but it is possible in JavaScript—thus, also in TypeScript. As a compile-time alternative, in C++ we can use multiple-inheritance to declare a type as a combination of two other types.
现在我们有了我们的extend()方法,我们可以更新我们的动物示例,如清单 8.13所示。Cat我们将 a 定义MeowingPet为 的子代,而不是Pet,它是一种可以meow()但还不完全是的动物Cat,因为它没有狩猎行为。接下来,我们可以将 a 定义Cat为 的交集MeowingPet & HuntingBehavior。每当我们想要创建一个新的实例时Cat,我们都会创建一个新的实例MeowingPet,并extend()使用一个新的实例HuntingBehavior。
Now that we have our extend() method, we can update our animal example as follows in listing 8.13. Instead of Cat, we define a MeowingPet as a child of Pet, which is an animal that can meow() but not quite a Cat yet, as it doesn’t have hunting behavior. Next, we can define a Cat as the intersection of MeowingPet & HuntingBehavior. Whenever we want to create a new instance of Cat, we create a new instance of MeowingPet and extend() it with a new instance of HuntingBehavior.
MeowingPet 类扩展宠物 { 1
喵():无效{
/* ... */
}
}
类 HunterBehavior { 2
跟踪():无效{
/* ... */
}
茎():无效{
/* ... */
}
突袭():无效{
/* ... */
}
}
类型 Cat = MeowingPet & HunterBehavior; 3个
const fluffy: Cat = extend(new MeowingPet(), new HunterBehavior()); 4个class MeowingPet extends Pet { 1
meow(): void {
/* ... */
}
}
class HunterBehavior { 2
track(): void {
/* ... */
}
stalk(): void {
/* ... */
}
pounce(): void {
/* ... */
}
}
type Cat = MeowingPet & HunterBehavior; 3
const fluffy: Cat = extend(new MeowingPet(), new HunterBehavior()); 4
我们可以将对的调用包装extend()在一个makeCat()函数中,这样可以更容易地创建Cat对象。与继承不同,通过使用混合,我们为行为的不同方面定义了不同的类型;然后我们将它们组合成一个完整的类型。我们通常有一些非常特定于一种特定类型的属性和方法——在我们的例子中,方法meow()——以及一些横切多种类型的属性和方法,例如多种动物的狩猎行为。
We can wrap the call to extend() in a makeCat() function, which makes it easier to create Cat objects. Unlike with inheritance, by using mix-ins, we define different types for different aspects of behavior; then we put them together into a complete type. We usually have some properties and methods that are very specific to one particular type—in our case, the meow() method—and some properties and methods that cross-cut across multiple types, such as the hunting behavior of multiple animals.
现在我们已经介绍了接口、继承、组合和混合——OOP 的主要元素——让我们看看纯面向对象代码的一些替代方案。
Now that we’ve covered interfaces, inheritance, composition, and mix-ins—the main elements of OOP—let’s look at a few alternatives to purely object-oriented code.
您将如何为也可以进行跟踪(通过某种updateStatus()方法)的运输信件和包裹建模?
How would you model shipping letters and packages that could also have tracking (through an updateStatus() method)?
面向对象编程非常有用。在隐藏实现细节的同时创建具有公共接口的组件并让它们相互交互的能力是管理复杂性以及划分和征服复杂领域的关键。
Object-oriented programming is extremely useful. The ability to create components with public interfaces while hiding the implementation details and have them interact with one another is key to managing complexity and dividing and conquering complex domains.
话虽这么说,但设计软件的方法有很多,正如我们在前几章的一些例子中看到的那样,这些例子展示了不同的设计模式,例如策略、装饰器和访问者。在某些情况下,替代方案提供了更好的解耦、组件化和可重用性。
That being said, there are more ways to design software, as we’ve seen with some of the examples in earlier chapters that showed different takes on design patterns, such as strategy, decorator, and visitor. In some cases, the alternatives offer better decoupling, componentization, and reusability.
替代方案不那么流行的原因是许多语言一开始都是纯粹面向对象的,不支持函数类型和泛型之类的东西。尽管他们中的大多数都进化为支持这些东西,但许多程序员仍然 几乎完全学习早期的纯面向对象的方法。让我们快速浏览一些可用的替代方案。
The reason why the alternatives are not as popular is that many languages started as purely object-oriented, without support for things like function types and generics. Although most of them evolved to support these things, many programmers are still learning almost exclusively the purely object-oriented methods of the earlier days. Let’s quickly go over a few available alternatives.
我们在第 3 章介绍了求和类型,当时我们研究了一种通过使用Variant和函数来实现访问者模式的方法visit()。下面快速回顾一下使用 OOP 和不使用 OOP 时代码的外观。
We covered sum types in chapter 3, when we looked at a way to implement the visitor pattern by using a Variant and a visit() function. Following is a quick refresher on how the code looked like with OOP and without it.
这次我们选择另一个场景:一个简单的 UI 框架。PanelUI 由、Label和对象树组成Button。在一种情况下,aRenderer将在屏幕上绘制这些元素。在第二种情况下,我们XmlSerializer会将 UI 树序列化为 XML,这样我们就可以保存它并在以后重新加载它。
We’ll pick another scenario this time: a simple UI framework. The UI consists of a tree of Panel, Label, and Button objects. In one scenario, a Renderer will draw these elements on the screen. In a second scenario, an XmlSerializer will serialize the UI tree as XML that so we can save it and reload it later.
请记住,我们可以在每个 UI 元素上添加一个渲染方法和一个序列化方法,但这种技术并不理想:无论何时我们想要添加另一个场景,我们都必须接触构成 UI 的所有类。这些类最终也对使用它们的环境了解得太多了。相反,我们可以使用一种访问者模式,它将场景与 UI 小部件分离,并让它们不关心它们将如何在我们的应用程序中使用,如以下清单所示。
Remember that we could add a method to render and a method to serialize on each of the UI elements, but that technique is not ideal: whenever we want to add another scenario, we have to touch all the classes that make up the UI. These classes also end up knowing way too much about the environment in which they are used. Instead, we can use a visitor pattern that will decouple the scenarios from the UI widgets and keep them oblivious to how they will be used in our application, as shown in the following listing.
界面 IVisitor {
访问面板(面板:面板):无效;
访问标签(标签:标签):无效;
访问按钮(按钮:按钮):无效;
}
类渲染器实现 IVisitor {
访问面板(面板:面板){ /* ... */ }
visitLabel(label: 标签) { /* ... */ }
visitButton(按钮:按钮) { /* ... */ }
}
类 XmlSerializer 实现 IVisitor {
访问面板(面板:面板){ /* ... */ }
visitLabel(label: 标签) { /* ... */ }
visitButton(按钮:按钮) { /* ... */ }
}
接口 IUIWidget {
接受(访客:IVisitor):无效;
}
类面板实现 IUIWidget {
/* 面板成员省略 */
接受(访客:IVisitor){
visitor.visitPanel(这个);
}
}
类标签实现 IUIWidget {
/* 标签成员省略 */
接受(访客:IVisitor){
visitor.visitLabel(这个);
}
}
类按钮实现 IUIWidget {
/* 按钮成员省略 */
接受(访客:IVisitor){
visitor.visitButton(这个);
}
}interface IVisitor {
visitPanel(panel: Panel): void;
visitLabel(label: Label): void;
visitButton(button: Button): void;
}
class Renderer implements IVisitor {
visitPanel(panel: Panel) { /* ... */ }
visitLabel(label: Label) { /* ... */ }
visitButton(button: Button) { /* ... */ }
}
class XmlSerializer implements IVisitor {
visitPanel(panel: Panel) { /* ... */ }
visitLabel(label: Label) { /* ... */ }
visitButton(button: Button) { /* ... */ }
}
interface IUIWidget {
accept(visitor: IVisitor): void;
}
class Panel implements IUIWidget {
/* Panel members omitted */
accept(visitor: IVisitor) {
visitor.visitPanel(this);
}
}
class Label implements IUIWidget {
/* Label members omitted */
accept(visitor: IVisitor) {
visitor.visitLabel(this);
}
}
class Button implements IUIWidget {
/* Button members omitted */
accept(visitor: IVisitor) {
visitor.visitButton(this);
}
}
在 OOP 实现中,我们需要IVisitor和IUIWidget接口将系统粘合在一起。所有 UI 小部件都需要知道如何IVisitor使事情正常进行,即使这不是必需的。
In the OOP implementation, we need IVisitor and IUIWidget interfaces to glue the system together. All UI widgets need to know about IVisitor to make things work, even though that shouldn’t be necessary.
另一种实现方式——使用 a——Variant消除了对接口的需求,并且文档项不需要知道访问者的存在。
The alternative implementation—using a Variant—removes the need for interfaces, and document items don’t need to know that visitors exist.
类渲染器{
renderPanel(panel: Panel) { /* ... */ }
renderLabel(label: 标签) { /* ... */ }
渲染按钮(按钮:按钮){ /* ... */ }
}
类 XmlSerializer {
serializePanel(panel: Panel) { /* ... */ }
serializeLabel(label: 标签) { /* ... */ }
序列化按钮(按钮:按钮){ /* ... */ }
}
类面板{
/* 面板成员省略 */
}
类标签{
/* 标签成员省略 */
}
类按钮{
/* 按钮成员省略 */
}
让小部件:变体<面板,标签,按钮> =
Variant.make1(新面板()); 1个
让序列化器:XmlSerializer = new XmlSerializer();
访问(小部件, 2
(面板:面板)=> serializer.serializePanel(面板),
(标签:标签)=> serializer.serializeLabel(标签),
(按钮:按钮)=> serializer.serializeButton(按钮)
);class Renderer {
renderPanel(panel: Panel) { /* ... */ }
renderLabel(label: Label) { /* ... */ }
renderButton(button: Button) { /* ... */ }
}
class XmlSerializer {
serializePanel(panel: Panel) { /* ... */ }
serializeLabel(label: Label) { /* ... */ }
serializeButton(button: Button) { /* ... */ }
}
class Panel {
/* Panel members omitted */
}
class Label {
/* Label members omitted */
}
class Button {
/* Button members omitted */
}
let widget: Variant<Panel, Label, Button> =
Variant.make1(new Panel()); 1
let serializer: XmlSerializer = new XmlSerializer();
visit(widget, 2
(panel: Panel) => serializer.serializePanel(panel),
(label: Label) => serializer.serializeLabel(label),
(button: Button) => serializer.serializeButton(button)
);
请注意,我们显示的是Variant和visit()正在使用,但从技术上讲,OOP 示例的等价物只是前五个类定义。请注意,不需要任何接口。
Note that we are showing the Variant and visit() being used, but technically, the equivalent of the OOP example is just the first five class definitions. Notice that no interfaces are needed.
一般来说,如果我们想以相同的方式传递不同类型的对象或将它们放在一个公共集合中,则它们不一定需要实现相同的接口或具有公共的父级。相反,我们可以使用 sum 类型,它可以在不强制类型之间存在任何关系的情况下实现相同的行为。
In general, if we want to pass around objects of different types in the same manner or put them in a common collection, they don’t necessarily need to implement the same interface or have a common parent. Instead, we can use a sum type, which enables the same behavior without enforcing any relationship between the types.
在 OOP 语言支持函数类型之前,我们必须将任何行为片段包装在一个类中。正如我们在第 5 章中看到的,一个典型的策略模式实现需要一个行为接口和几个类来实现该接口。
Before OOP languages supported function types, we had to wrap any piece of behavior in a class. As we saw in chapter 5, a typical strategy pattern implementation required an interface for the behavior and several classes to implement the interface.
让我们回顾一下第 5 章中的图,其中描述了策略模式的两种替代实现(图 8.9)。
Let’s review the figures from chapter 5, which described the two alternative implementations for the strategy pattern (figure 8.9).
如果我们可以将算法实现作为函数传递,这可以简化很多。我们使用函数类型代替接口;我们使用函数代替类(图 8.10)。
This can be simplified a lot if we can just pass the algorithm implementation as a function. Instead of an interface, we use a function type; instead of classes, we use functions (figure 8.10).
函数式编程也避免维护状态:一个函数可以接受一组参数,执行一些计算,并在不改变任何状态的情况下返回结果。
Functional programming also avoids maintaining state: a function can take a set of arguments, perform some computation, and return the result without changing any state.
让我们重新审视清单 8.16中的二进制表达式示例,看看函数式实现的样子。如果我们将表达式定义为计算结果为数字的东西,我们可以将 our 替换为不带参数并返回数字的IExpression函数类型。Expression代替 a SumExpression,我们可以实现一个工厂函数makeSumExpression(),给定两个数字,返回一个可以将它们相加的闭包。请记住闭包捕获状态——在本例中为 a和b参数。乘法也是如此。
Let’s revisit our binary expression example in listing 8.16 and see how a functional implementation would look. If we define an expression as something that evaluates to a number, we can replace our IExpression with a function type Expression that takes no arguments and returns a number. Instead of a SumExpression, we can implement a factory function makeSumExpression() that, given two numbers, returns a closure that can add them up. Remember that a closure captures state—in this case, the a and b arguments. The same is true for multiplication.
输入表达式 = () => 数字; 1个
function makeSumExpression(a: number, b: number): 表达式 {
返回 () => a + b; 2个
}
函数 makeMulExpression(a: 数字, b: 数字): 表达式 {
返回 () => a * b; 3
}type Expression = () => number; 1
function makeSumExpression(a: number, b: number): Expression {
return () => a + b; 2
}
function makeMulExpression(a: number, b: number): Expression {
return () => a * b; 3
}
我们不再需要BinaryExpression;该类过去用于保存状态,但现在状态被包裹在闭包中。
We no longer need BinaryExpression; that class used to hold state, but now state is wrapped in the closures.
如果我们IExpression更复杂,声明多个方法,那么面向对象的方法可能会更好。但是请留意一些简单的情况,在这些情况下,您可以使用函数式方法以更少的代码实现相同的行为。
If our IExpression were more complex, declaring multiple methods, the object-oriented approach might have worked better. But keep an eye out for simple cases in which you can achieve the same behavior with much less code by using a functional approach.
纯面向对象编程的另一种选择是泛型编程。到目前为止,我们已经在许多代码示例中使用了泛型,但还没有深入介绍它们。我们将在接下来的两章中这样做,我们将看到抽象和重用代码的不同方法。
The other alternative to purely object-oriented programming is generic programming. We’ve used generics in many code examples thus far but haven’t covered them in depth yet. We’ll do that in the next two chapters, and we’ll see different ways to abstract and reuse code.
本节的要点不应该是避免面向对象编程;它是我们可以用来解决范围广泛的问题的重要工具。得出的结论是,我们应该牢记一些替代方案。我们应该选择使我们的代码尽可能安全、清晰和松散耦合的方法。
The takeaway from this section shouldn’t be to avoid object-oriented programming; it is an important tool that we can use to solve a broad range of problems. The takeaway is that there are alternatives that we should keep in mind. We should pick the approach that makes our code as safe, as clear, and as loosely coupled as possible.
在本章中我们只简单地谈到了泛型,因为接下来的两章将专门讨论这个主题。继续阅读!
We touched only briefly on generics in this chapter, as the next two chapters will focus exclusively on that topic. Read on!
c—从功能的角度来看index(),这显然是一个契约,所以期待一个INamed接口是方法。
c—From the point of view of the index() function, this is clearly a contract, so expecting an INamed interface is the approach.
我们可以简单地通过组合其他两个接口来定义这个接口:
接口 IterableIterator<T> 扩展 Iterable<T>, Iterator<T> { }
We can define this interface simply by combining the two other interfaces:
interface IterableIterator<T> extends Iterable<T>, Iterator<T> { }
d——即使只看类名,我们也可以看出这三个例子都没有描述 is -a关系,所以它们看起来都不像继承的好用法。
d—Even by just seeing the class name, we can tell that none of the three examples describe an is-a relationship, so none of them look like a good use of inheritance.
使用继承的可能实现:
抽象类 UnaryExpression 实现 IExpression { 只读一个:数字; 构造函数(a:数字){ 这个.a = a; } 抽象评估():数字; } 类 UnaryMinusExpression 扩展 UnaryExpression { 评估():数字{ 返回 -this.a; } }
A possible implementation using inheritance:
abstract class UnaryExpression implements IExpression { readonly a: number; constructor(a: number) { this.a = a; } abstract eval(): number; } class UnaryMinusExpression extends UnaryExpression { eval(): number { return -this.a; } }
c—这个场景很适合使用组合。Connection应该是 的成员FileTransfer,因为 需要它FileTransfer,但两种类型都不应直接扩展另一种。
c—This scenario is a good one for using composition. Connection should be a member of FileTransfer, as it is needed by FileTransfer, but neither type should directly extend the other.
使用组合的可能实现:
类翼{ 只读引擎:Engine = new Engine(); } 类飞机{ 只读 leftWing: Wing = new Wing(); 只读 rightWing: Wing = new Wing(); }
A possible implementation using composition:
class Wing { readonly engine: Engine = new Engine(); } class Airplane { readonly leftWing: Wing = new Wing(); readonly rightWing: Wing = new Wing(); }
对此建模的一种方法是在类中提供跟踪行为,然后将其与类Tracking混合以向它们添加跟踪行为。在 TypeScript 中,这可以通过如下方法完成: LetterPackageextend()
类字母 { /*...*/ } 类包 { /*...*/ } 类跟踪{ setStatus(status: Status) { /*...*/ } } 输入 LetterWithTracking = Letter & Tracking; 输入 PackageWithTracking = 包裹和跟踪;
One way to model this is to provide tracking behavior in a Tracking class and then mix it in with Letter and Package classes to add tracking behavior to them. In TypeScript, this can be done with a method like extend():
class Letter { /*...*/ } class Package { /*...*/ } class Tracking { setStatus(status: Status) { /*...*/ } } type LetterWithTracking = Letter & Tracking; type PackageWithTracking = Package & Tracking;
本章涵盖
This chapter covers
我们将从涵盖应该使用它们的常见情况开始我们对泛型类型的讨论:制作独立的、可重用的组件。我们将查看几个可以从身份函数(仅返回其参数的函数)中受益的场景,并查看此类函数的通用实现。我们还将回顾Optional<T>我们在第 3 章中构建的类型,作为另一种简单但功能强大的泛型类型。
We’ll start our discussion of generic types by covering a common case in which they should be used: making independent, reusable components. We’ll look at a couple of scenarios in which we would benefit from an identity function (a function that simply returns its argument) and see a generic implementation of such a function. We’ll also review the Optional<T> type we built in chapter 3 as another simple but powerful generic type.
接下来,我们将讨论数据结构。数据结构可以塑造我们的数据,而不必知道数据是什么。使这些结构通用化允许我们为各种值重用形状,显着减少我们需要编写的代码量。我们将从数字的二叉树和字符串的链表开始,并从中派生出通用的二叉树和链表。
Next, we’ll talk about data structures. Data structures give shape to our data without having to be aware of what the data is. Making these structures generic allows us to reuse the shape for all sorts of values, significantly reducing the amount of code we need to write. We’ll start with a binary tree of numbers and a linked list of strings, and derive a generic binary tree and linked list from them.
通用数据结构并不能解决我们所有的问题:我们仍然需要遍历它们。我们将看到如何使用迭代器为遍历任何数据结构提供通用接口。这也有助于我们减少所需的代码量,因为我们不必为每个数据结构提供不同版本的函数,而是提供与迭代器一起使用的单一版本。同样,我们将使用我们在第 6 章中介绍的生成器。这些可恢复函数产生值,我们可以使用它们在我们的数据结构上实现迭代器。
Generic data structures don’t solve all our problems: we still need to traverse them. We’ll see how we can use iterators to provide a common interface for traversing any data structure. This also helps us reduce the amount of code we need, as we don’t have to provide different versions of functions for each data structure, but a single version that works with iterators. Again, we’ll use generators, which we introduced in chapter 6. These resumable functions yield values, and we can use them to implement iterators over our data structures.
最后,我们将讨论将函数链接到处理管道中并在可能无限的数据流上运行它们。
Finally, we’ll talk about chaining functions into processing pipelines and running them over potentially infinite streams of data.
让我们用一个简单的例子来介绍泛型:我们有一个函数 ,getNumbers()它给我们一个数字数组,但允许我们在返回它们之前对它们应用转换。这是通过transform()接受一个数字并返回一个数字的参数来完成的。调用者可以传入这样一个transform()函数,并getNumbers()在返回结果之前应用它,如下一个清单所示。
Let’s introduce generics with a simple example: we have a function, getNumbers(), that gives us an array of numbers but allows us to apply a transformation to them before returning them. This is done with a transform() argument that takes a number and returns a number. Callers can pass in such a transform() function, and getNumbers() will apply it before returning its result, as shown in the next listing.
输入 TransformFunction = (value: number) => number; 1个
函数 getNumbers(
变换:变换函数):数字[] { 2
/* ... */
}type TransformFunction = (value: number) => number; 1
function getNumbers(
transform: TransformFunction): number[] { 2
/* ... */
}
如果调用方不需要应用任何转换怎么办?一个好的默认值transform()是一个不做任何事情的函数——一个只返回其结果的函数,如以下清单所示。
What if the callers don’t need to apply any transformation? A good default for this transform() would be a function that doesn’t do anything—one that simply returns its result, as shown in the following listing.
输入 TransformFunction = (value: number) => number;
函数 doNothing(值:数字):数字 { 1
返回值;
}
函数 getNumbers(
变换:TransformFunction = doNothing ): number[] { 2
/* ... */
}type TransformFunction = (value: number) => number;
function doNothing(value: number): number { 1
return value;
}
function getNumbers(
transform: TransformFunction = doNothing): number[] { 2
/* ... */
}
让我们看另一个例子。假设我们有一个对象数组和一种从对象Widget创建对象的方法。函数处理对象数组并返回对象数组。因为我们不想组装超过需要的东西,所以将一个函数作为参数,给定一个对象数组,该函数返回该数组的一个子集,如以下代码所示。这允许调用者告诉函数哪些小部件真正需要组装,因此其余的可以忽略。 AssembledWidgetWidgetassembleWidgets()WidgetAssembledWidgetassembleWidgets()pluck()Widget
Let’s look at another example. Assume that we have an array of Widget objects and a way to create an AssembledWidget object out of a Widget object. An assembleWidgets() function handles an array of Widget objects and returns an array of AssembledWidget objects. Because we don’t want to assemble more than needed, assembleWidgets() takes as argument a pluck() function, which, given an array of Widget objects, returns a subset of this array, as shown in the following code. This allows callers to tell the function which widgets really need assembling, so the rest can be ignored.
输入 PluckFunction = (widgets: Widget[]) => Widget[]; 1个
函数 assembleWidgets(
采摘: PluckFunction): AssembledWidget[] { 2
/* ... */
}type PluckFunction = (widgets: Widget[]) => Widget[]; 1
function assembleWidgets(
pluck: PluckFunction): AssembledWidget[] { 2
/* ... */
}
这个函数的默认值是什么pluck()?我们可以说,如果调用者不提供函数pluck(),我们将转换整个小部件列表。让我们调用这个默认值pluckAll()并让它在下一个清单中简单地返回它的参数。
What would be a good default for this pluck() function? We can say that if the caller doesn’t supply a pluck() function, we transform the whole list of widgets. Let’s call this default pluckAll() and have it simply return its argument in the next listing.
输入 PluckFunction = (widgets: Widget[]) => Widget[];
function pluckAll(widgets: Widget[]): Widget[] { 1
返回小部件;
}
函数 assembleWidgets(
采摘: PluckFunction = pluckAll ): AssembledWidget[] { 2
/* ... */
}type PluckFunction = (widgets: Widget[]) => Widget[];
function pluckAll(widgets: Widget[]): Widget[] { 1
return widgets;
}
function assembleWidgets(
pluck: PluckFunction = pluckAll): AssembledWidget[] { 2
/* ... */
}
并排查看我们的两个示例,我们可以看到doNothing()和pluckAll()非常相似:它们都接受一个参数并在不进行任何处理的情况下返回它,如以下清单所示。
Looking at our two examples side by side, we can see that doNothing() and pluckAll() are very similar: they both take an argument and return it without doing any processing, as the following listing shows.
函数 doNothing(值:数字):数字 {
返回值;
}
function pluckAll(widgets: Widget[]): Widget[] {
返回小部件;
}function doNothing(value: number): number {
return value;
}
function pluckAll(widgets: Widget[]): Widget[] {
return widgets;
}
它们之间的区别在于它们获取和返回值的类型:do-Nothing()使用数字,并pluckAll()使用对象数组Widget。这两个函数都是恒等函数。在代数中,恒等函数是一个函数f(x) = x。
The difference between them is the type of the value they take and return: do-Nothing() uses a number, and pluckAll() uses an array of Widget objects. Both functions are identity functions. In algebra, an identity function is a function f(x) = x.
我们必须创建两个非常相似的独立函数,这不是很好。这种方法不能很好地扩展。我们可以通过编写一个可重用的身份函数来简化这个过程吗?答案是肯定的。
It’s not great that we had to create two separate functions that are so similar. This approach doesn’t scale well. Can we simplify this process by writing a reusable identity function? The answer is yes.
让我们从一种简单的方法开始,因为身份对于任何类型都是相同的,所以我们简单地使用any. 这将为我们提供一个identity()函数,该函数接受 type 的值any并返回 type 的值any,如下一个清单所示。
Let’s start with a naïve approach and say that because identity is the same for any type, we simply use any. This would give us an identity() function that takes a value of type any and returns a value of type any, as shown in the next listing.
函数标识(值:任何):任何{
返回值;
}function identity(value: any): any {
return value;
}
此实现的问题在于,当我们开始使用 时any,我们绕过了类型检查器并失去了类型安全性,如以下清单所示。identity()我们可以将使用字符串调用的 结果传递给需要数字的函数,代码编译得很好,但在运行时会失败。
The problem with this implementation is that when we start using any, we bypass the type checker and lose type safety, as shown in the following listing. We can pass the result of calling identity() with a string to a function that expects a number, and the code will compile just fine, but it will fail at run time.
函数平方(x:数字):数字{
返回 x * x;
}
广场(身份(“你好!”)); 1个function square(x: number): number {
return x * x;
}
square(identity("Hello!")); 1
有一种更安全的方法可以做到这一点:参数化函数之间的不同之处,即参数的类型。该参数将是一个类型参数。
There is a safer way to do this: parameterize what is different between the functions, namely the type of their argument. This parameter will be a type parameter.
类型参数是泛型类型名称的标识符。类型参数用作客户端在创建泛型实例时指定的特定类型的占位符。
A type parameter is an identifier for a generic type name. Type parameters are used as placeholders for specific types that the client specifies when creating an instance of the generic type.
在下一个清单中,我们的通用身份将使用类型参数T,这将number在第一种情况和Widget[]第二种情况下出现。
In the next listing, our generic identity will use a type parameter T, which will be number in the first case and Widget[] in the second case.
函数标识<T>(值: T): T { 1
返回值;
}
函数 getNumbers(
转换:TransformFunction = identity):number[] { 2
/* ... */
}
函数 assembleWidgets(
采摘: PluckFunction = identity ): AssembledWidget[] { 3
/* ... */
}function identity<T>(value: T): T { 1
return value;
}
function getNumbers(
transform: TransformFunction = identity): number[] { 2
/* ... */
}
function assembleWidgets(
pluck: PluckFunction = identity): AssembledWidget[] { 3
/* ... */
}
T编译器足够聪明,无需我们拼写就可以弄清楚应该是什么。我们不再需要doNothing()and pluckAll(),如果我们需要恒等函数,我们可以将它与任何其他类型重用。现在,当确定类型时,例如当getNumbers()caseT为 时number,编译器可以执行类型检查,并且我们不再像尝试字符串那样结束square(),如下一个清单所示。
The compiler is smart enough to figure out what T should be without our having to spell it out. We no longer need doNothing() and pluckAll(), and we can reuse this with any other type if we need an identity function. Now when the type is determined, such as when the getNumbers() case T is number, the compiler can perform type checking, and we no longer end up in a situation like attempting to square() a string, as shown in the next listing.
函数标识<T>(值:T):T {
返回值;
}
广场(身份(“你好!”)); 1个function identity<T>(value: T): T {
return value;
}
square(identity("Hello!")); 1
我们可以想出这个实现,因为无论使用何种类型的函数,恒等函数的机制都是相同的。getNumbers()我们有效地将恒等逻辑与和的问题域分离,assembleWidgets()因为恒等逻辑和问题域是正交的,或者说是独立的。
We could come up with this implementation because the mechanics of the identity function are the same regardless of the type the function is used with. We effectively decoupled the identity logic from the problem domain of getNumbers() and assembleWidgets() because the identity logic and the problem domain are orthogonal, or independent.
作为另一个例子,看看Optional我们在第 3 章中提供的实现。请记住,可选类型包含某种类型的值T或不包含任何内容。
As another example, take a look at the Optional implementation we provided in chapter 3. Remember that an optional type contains a value of some type T or doesn’t contain anything.
可选类 <T> { 1
私有值:T | 不明确的;
私人分配:布尔值;
构造函数(值?:T){ 2
如果(值){
this.value = 值;
this.assigned = true;
} 别的 {
this.value = undefined;
this.assigned = false;
}
}
有值():布尔值{
返回this.assigned;
}
getValue(): T {
如果(!this.assigned)抛出错误(); 3个
返回<T>这个值;
}
}class Optional<T> { 1
private value: T | undefined;
private assigned: boolean;
constructor(value?: T) { 2
if (value) {
this.value = value;
this.assigned = true;
} else {
this.value = undefined;
this.assigned = false;
}
}
hasValue(): boolean {
return this.assigned;
}
getValue(): T {
if (!this.assigned) throw Error(); 3
return <T>this.value;
}
}
同样,处理值缺失的逻辑独立于值的实际类型。我们有一个Optional可以存储任何其他类型的通用类型,因为它将以相同的方式处理任何事情。你可以认为Optional是在一个完全不同的维度中T,因为我们所做的任何改变都Optional不会影响T,而任何改变都T不会影响Optional。这种隔离是泛型编程的一个极其强大的特性。
The logic of handling the absence of a value is, again, independent of the actual type of the value. We have a generic Optional type that can store any other type, as it will handle anything in the same way. You can think of Optional as being in a completely different dimension from T, as any changes we make to Optional do not affect T, and any changes made to T do not affect Optional. This isolation is an extremely powerful feature of generic programming.
我们刚刚看到了泛型的两种用法:泛型函数和泛型类。现在让我们退后一步,看看是什么让泛型类型如此特殊。我们通过查看基本类型和组合它们的方法开始本书。我们有诸如booleanand之类的类型number,以及诸如boolean | number. 我们有函数类型,例如() => number. 正如我们所见,这些类型都没有任何类型参数。一个数字就是一个数字。返回数字的函数是返回数字的函数。
We just saw two uses of generics: a generic function and a generic class. Now let’s step back and look at what makes generic types special. We started the book by looking at basic types and ways to combine them. We have types such as boolean and number, and types such as boolean | number. We have function types such as () => number. As we can see, none of these types has any type parameter. A number is a number. A function that returns a number is a function that returns a number.
当我们引入泛型时,这种情况发生了变化。我们有一个(value: T) => T带有类型参数的通用函数T。当我们为 指定实际类型时,我们创建了特定的函数T。Widget[]例如,如果我们使用,我们最终会得到一个函数类型(value: Widget[]) => Widget[]。这是我们第一次可以插入类型并获得不同的类型定义(图 9.1)。
When we introduce generics, this situation changes. We have a generic function (value: T) => T, with a type parameter T. We create specific functions when we specify an actual type for T. If we use Widget[], for example, we end up with a function type (value: Widget[]) => Widget[]. This is the first time when we can plug in types and get different type definitions (figure 9.1).
泛型类型是在一个或多个类型上参数化的泛型函数、类、接口等。通用类型允许我们编写适用于不同类型的通用代码,从而实现高水平的代码重用。
A generic type is a generic function, class, interface, and so on that is parameterized over one or more types. Generic types allow us to write general code that works with different types, enabling a high level of code reuse.
正如我们在前面的示例中看到的,以及我们将在本章和下一章中看到的那样,能够使用泛型可以使我们的代码更好地组件化。我们可以将这些通用组件用作构建块,并将它们组合起来以实现所需的行为,同时将它们之间的依赖性降到最低。超越我们的简单identity<T>()和Optional<T>示例,让我们看看数据结构。
As we saw in the previous examples, and as we’ll see throughout this chapter and the next, being able to use generics makes our code much better componentized. We can use these generic components as building blocks and combine them to achieve the desired behavior while having minimal dependency between them. Moving beyond our simple identity<T>() and Optional<T> examples, let’s look at data structures.
实现一个Box<T>简单地包装 type 值的泛型T。
Implement a generic Box<T> type that simply wraps a value of type T.
实现一个unbox<T>()接受 aBox<T>并返回装箱值的通用函数。
Implement a generic unbox<T>() function that takes a Box<T> and returns the boxed value.
让我们从几个非泛型的例子开始:一棵数字的二叉树,如清单 9.11所示,以及一个字符串链表,如清单 9.12所示。我相信您熟悉这些简单的数据结构。我们将树实现为一个或多个节点,每个节点存储一个数值和对其左右子节点的引用。这些引用可以指向节点,或者undefined在没有子节点的情况下。
Let’s start with a couple of nongeneric examples: a binary tree of numbers, shown in listing 9.11, and a linked list of strings shown in listing 9.12. I’m sure that you are familiar with these simple data structures. We will implement the tree as one or more nodes, each node storing a number value and references to its left and right children. These refences can be to nodes or undefined in case there is no child node.
类 NumberBinaryTreeNode {
值:数字;
左:NumberBinaryTreeNode | 不明确的;
右:NumberBinaryTreeNode | 不明确的;
构造函数(值:数字){
this.value = 值;
}
}class NumberBinaryTreeNode {
value: number;
left: NumberBinaryTreeNode | undefined;
right: NumberBinaryTreeNode | undefined;
constructor(value: number) {
this.value = value;
}
}
我们将类似地将链表实现为一个或多个节点,每个节点存储一个string和一个对下一个节点的引用,或者undefined如果没有下一个节点,如下一个清单所示。
We will similarly implement the linked list as one or more nodes, each storing a string and a reference to the next node, or undefined if there is no next node, as the next listing shows.
类 StringLinkedListNode {
值:字符串;
下一个:StringLinkedListNode | 不明确的;
构造函数(值:字符串){
this.value = 值;
}
}class StringLinkedListNode {
value: string;
next: StringLinkedListNode | undefined;
constructor(value: string) {
this.value = value;
}
}
现在,如果我们在项目的另一部分需要一个字符串的二叉树怎么办?我们可以实现一个StringBinaryTreeNode与 相同的NumberBinaryTreeNode值类型并将其替换number为string。这很诱人,因为我们可以复制/粘贴代码并替换一些东西,但复制/粘贴从来都不是一个好的选择。想象一下,我们的类也有一堆方法。如果我们复制/粘贴这些方法,然后在其中一个版本中发现错误,我们很可能会错过修复复制/粘贴版本中的错误。我们确定您知道这是怎么回事:我们可以使用泛型而不是重复!
Now what if we need, in another part of our project, a binary tree of strings? We can implement a StringBinaryTreeNode that is identical to NumberBinaryTreeNode and replace the type of value from number to string. This is tempting, as we can just copy/paste the code and replace a couple of things, but copy/pasting is never a good option. Imagine that our class also has a bunch of methods. If we copied/pasted those methods and then found a bug in one of the versions, we’d likely miss fixing the bug in the copied/pasted version. We’re sure you see where this is going: we can use generics instead of duplication!
我们可以实现适用于任何类型的泛型BinaryTreeNode<T>,如下一个清单所示。
We can implement a generic BinaryTreeNode<T> that works for any type, as shown in the next listing.
类 BinaryTreeNode<T> {
值:T; 1个
左:BinaryTreeNode<T> | 不明确的;
右:BinaryTreeNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
}class BinaryTreeNode<T> {
value: T; 1
left: BinaryTreeNode<T> | undefined;
right: BinaryTreeNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
}
事实上,我们不应该等待新的要求有一个字符串的二叉树:我们原来的NumberBinaryTreeNode实现在二叉树数据结构和类型之间有一个不必要的耦合number。同样,我们可以将 our 替换StringLinkedListNode为通用的LinkedListNode<T>,如以下清单所示。
In fact, we shouldn’t wait for the new requirement to have a binary tree of strings to come in: our original NumberBinaryTreeNode implementation has an unnecessary coupling between the binary tree data structure and the type number. Similarly, we can replace our StringLinkedListNode with a generic LinkedListNode<T>, shown in the following listing.
类 LinkedListNode<T> {
值:T;
下一个:LinkedListNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
}class LinkedListNode<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
}
请记住,大多数语言的库已经提供了您需要的大部分数据结构(列表、队列、堆栈、集合、字典等)。我们正在研究实现以更好地理解泛型,但最好的办法是根本不编写代码。如果我们可以从库中选择一个通用数据结构,我们就应该这样做。
Do keep in mind that most languages have libraries that already provide most of the data structures you need (lists, queues, stacks, sets, dictionaries, and so on). We’re going over implementations to better understand generics, but the best thing to do is not to write code at all. If we can choose a generic data structure from a library, we should do that.
让我们有点哲学性地问“数据结构的本质是什么?” 一个数据结构由三部分组成:
Let’s get a bit philosophical and ask “What is the nature of a data structure?” A data structure consists of three parts:
这里有两个不同的问题。一个是数据——数据的类型和数据结构实例所持有的实际值。另一个是数据的形状和保形操作。像我们在本节开头看到的那些通用数据结构帮助我们解耦这些问题。通用数据结构处理数据的布局、其形状和任何形状保持操作。二叉树是二叉树,不管它包含的是字符串还是数字。我们可以通过将数据布局的责任转移到独立于实际数据内容的通用数据结构来组件化我们的代码。
There are two separate concerns here. One is the data—the type of the data and the actual value that an instance of the data structure holds. The other is the shape of the data and the shape-preserving operations. Generic data structures like the ones we saw at the beginning of this section help us decouple these concerns. A generic data structure handles the layout of the data, its shape, and any shape-preserving operations. A binary tree is a binary tree regardless of whether it contains strings or numbers. We can componentize our code by moving the responsibility for data layout to generic data structures that are independent of the actual data content.
假设我们有所有这些数据结构,让我们看看如何遍历它们并查看它们的内容。
Assuming that we have all these data structures, let’s look at how we can traverse them and view their content.
使用通用的、和方法 实现Stack<T>表示堆栈(后进先出)的数据结构。push()pop()peek()
Implement a Stack<T> data structure representing a stack (last-in-first-out) with the common push(), pop(), and peek() methods.
使用和两种类型的成员 实现一个Pair<T, U>数据结构。firstsecond
Implement a Pair<T, U> data structure with first and second members of the two types.
假设我们要按顺序遍历二叉树并打印其所有元素的值,如清单 9.15所示。快速提醒一下,中序遍历是递归遍历左-父-右(图 9.2)。
Let’s say we want to traverse our binary tree in order and print the value of all its elements, as shown in listing 9.15. As a quick reminder, an in-order traversal is the recursive traversal left–parent–right (figure 9.2).
类 BinaryTreeNode<T> { 1
值:T;
左:BinaryTreeNode<T> | 不明确的;
右:BinaryTreeNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
}
函数 printInOrder<T>(root: BinaryTreeNode<T>): void {
if (root.left != undefined) { 2
printInOrder(root.left);
}
控制台日志(根值); 3个
if (root.right != undefined) { 4
printInOrder(root.right);
}
}class BinaryTreeNode<T> { 1
value: T;
left: BinaryTreeNode<T> | undefined;
right: BinaryTreeNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
}
function printInOrder<T>(root: BinaryTreeNode<T>): void {
if (root.left != undefined) { 2
printInOrder(root.left);
}
console.log(root.value); 3
if (root.right != undefined) { 4
printInOrder(root.right);
}
}
作为示例,让我们创建一个包含几个节点的树,并查看printInOrder()以下代码中返回的内容。
As an example, let’s create a tree with a few nodes and see what printInOrder() returns in the following code.
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1); root.left = new BinaryTreeNode(2); root.left.right = new BinaryTreeNode(3); root.right = new BinaryTreeNode(4); 打印订单(根);
let root: BinaryTreeNode<number> = new BinaryTreeNode(1); root.left = new BinaryTreeNode(2); root.left.right = new BinaryTreeNode(3); root.right = new BinaryTreeNode(4); printInOrder(root);
这段代码创建了图 9.3所示的树。
This code creates the tree shown in figure 9.3.
按顺序遍历它会打印
Traversing it in order will print
2个 3个 1个 4个
2 3 1 4
如果我们还想打印字符串链表的所有值怎么办?我们可以实现一个printList()函数,从头到尾遍历列表并打印每个元素,如下一个清单所示。
What if we also want to print all the values of a linked list of strings? We can implement a printList() function that traverses a list from head to tail and prints each element, as the next listing shows.
类 LinkedListNode<T> { 1
值:T;
下一个:LinkedListNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
}
函数 printLinkedList<T>(head: LinkedListNode<T>): void {
让当前:LinkedListNode<T> | 未定义=头; 2个
while (current) { 3
console.log(current.value); 4
current = current.next; 4个
}
}class LinkedListNode<T> { 1
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
}
function printLinkedList<T>(head: LinkedListNode<T>): void {
let current: LinkedListNode<T> | undefined = head; 2
while (current) { 3
console.log(current.value); 4
current = current.next; 4
}
}
举一个具体的例子,我们可以初始化一个字符串列表并使用 打印它printLinkedList(),如下面的清单所示。
Taking a concrete example, we can initialize a list of strings and print it by using printLinkedList(), shown in the following listing.
让 head: LinkedListNode<string> = new LinkedListNode("Hello");
head.next = new LinkedListNode("世界");
head.next.next = new LinkedListNode("!!!");
打印链表(头);let head: LinkedListNode<string> = new LinkedListNode("Hello");
head.next = new LinkedListNode("World");
head.next.next = new LinkedListNode("!!!");
printLinkedList(head);
此代码创建如图 9.4所示的列表。
This code creates the list shown in figure 9.4.
运行代码将打印
Running the code will print
你好 世界 !!!
Hello World !!!
这行得通,但也许有更好的方法。
This works, but maybe there is a better way.
如果我们可以根据职责进一步拆分代码会怎样?我们的print-InOrder()andprintLinkedList()函数执行两个任务:遍历数据结构并打印其内容。更糟糕的是,第二个任务重叠;这两个函数都打印值。
What if we could further split the code apart based on responsibilities? Our print-InOrder() and printLinkedList() functions perform two tasks: traverse a data structure and print its contents. Even worse, the second task overlaps; both functions print values.
我们可以再做一个概括。让我们将遍历移动到它自己的组件。让我们从我们的二叉树开始。我们需要一种方法来按顺序遍历树中的每个项目并返回每个节点的值。我们可以称这种遍历迭代;我们正在迭代数据结构。
We can make another generalization. Let’s move traversal to its own component. Let’s start with our binary tree. We need a way to go over every item in the tree in order and return the value of each node. We can call this traversal iteration; we are iterating over the data structure.
迭代器是一种能够遍历数据结构的对象。它提供了一个标准接口,可以对客户端隐藏数据结构的实际形状。
An iterator is an object that enables traversal of a data structure. It provides a standard interface that hides the actual shape of the data structure from the clients.
让我们实现我们的迭代器。我们首先将 an 定义IteratorResult<T>为包含两个属性的类型:一个value类型属性T和一个简单地告诉我们是否已经到达终点的done类型属性boolean,如以下清单所示。
Let’s implement our iterators. We’ll start by defining an IteratorResult<T> as a type that contains two properties: a value property of type T and a done property of type boolean that simply tells us whether we’ve reached the end, as shown in the following listing.
类型 IteratorResult<T> = {
完成:布尔值;
值:T;
}type IteratorResult<T> = {
done: boolean;
value: T;
}
在下一个清单中,定义一个Iterator<T>声明单个next()方法的迭代器接口。此方法返回一个IteratorResult<T>.
In the next listing, define an iterator interface Iterator<T> that declares a single next() method. This method returns an IteratorResult<T>.
接口迭代器<T> {
下一个():迭代器结果<T>;
}interface Iterator<T> {
next(): IteratorResult<T>;
}
现在我们可以将 a 实现BinaryTreeNodeIterator<T>为类实现,如代码清单 9.21Iterator<T>所示。我们正在使用私有方法进行有序遍历,并将所有节点值推送到队列中。该方法通过使用数组方法使值出列并返回值,直到没有更多值可返回(图 9.5)。 inOrder()next()shift()IteratorResult<T>
Now we can implement a BinaryTreeNodeIterator<T> as a class implementing Iterator<T>, as shown in listing 9.21. We’re doing an in-order traversal with the private method inOrder() and pushing all node values to a queue. The next() method dequeues the values by using the array shift() method and returns IteratorResult<T> values until there are no more values to return (figure 9.5).
类 BinaryTreeIterator<T> 实现 Iterator<T> {
私有值:T[]; 1个
私有根:BinaryTreeNode<T>;
构造函数(根:BinaryTreeNode<T>){
this.values = [];
this.root = root;
这个.inOrder(根); 2个
}
下一个():迭代器结果<T> {
常量结果:T | undefined = this.values.shift(); 3个
如果(!结果){
return { done: true, value: this.root.value }; 4个
}
返回{完成:假,值:结果};
}
private inOrder(node: BinaryTreeNode<T>): void { 5
如果(node.left!=未定义){
这个.inOrder(node.left);
}
this.values.push(node.value); 6个
如果(node.right!=未定义){
this.inOrder(node.right);
}
}
}class BinaryTreeIterator<T> implements Iterator<T> {
private values: T[]; 1
private root: BinaryTreeNode<T>;
constructor(root: BinaryTreeNode<T>) {
this.values = [];
this.root = root;
this.inOrder(root); 2
}
next(): IteratorResult<T> {
const result: T | undefined = this.values.shift(); 3
if (!result) {
return { done: true, value: this.root.value }; 4
}
return { done: false, value: result };
}
private inOrder(node: BinaryTreeNode<T>): void { 5
if (node.left != undefined) {
this.inOrder(node.left);
}
this.values.push(node.value); 6
if (node.right != undefined) {
this.inOrder(node.right);
}
}
}
这个实现不是最有效的,因为我们需要一个队列,其元素数量与树中的节点数量相同。我们可以做一个更高效的遍历,需要更少的内存,但逻辑会变得更复杂。让我们现在以此为例,因为我们很快就会看到更好、更简单的方法来做到这一点。
This implementation is not the most efficient, as we need a queue with the same number of elements as the number of nodes in the tree. We can do a more efficient traversal that requires less memory, but the logic gets more complex. Let’s use this for now as an example, as we’ll soon see a better and simpler way to do this.
让我们LinkedListIterator<T>在下一个清单中实现遍历链表。
Let’s also implement the LinkedListIterator<T> to traverse our linked list in the next listing.
类 LinkedListIterator<T> 实现 Iterator<T> {
私有头:LinkedListNode<T>;
私人电流:LinkedListNode<T> | 不明确的;
构造函数(头:LinkedListNode<T>){
this.head = 头;
this.current = head;
}
下一个():迭代器结果<T> {
如果(!this.current){
return { done: true, value: this.head.value }; 1个
}
常量结果:T = this.current.value; 2
this.current = this.current.next; 3
返回{完成:假,值:结果}; 4个
}
}class LinkedListIterator<T> implements Iterator<T> {
private head: LinkedListNode<T>;
private current: LinkedListNode<T> | undefined;
constructor(head: LinkedListNode<T>) {
this.head = head;
this.current = head;
}
next(): IteratorResult<T> {
if (!this.current) {
return { done: true, value: this.head.value }; 1
}
const result: T = this.current.value; 2
this.current = this.current.next; 3
return { done: false, value: result }; 4
}
}
完成管道后,让我们看看为什么这些迭代器很有用。如果我们想要打印二叉树中所有节点的值以及字符串链表中所有字符串的值,我们不再需要单独的函数。我们可以使用一个带有迭代器参数的通用函数,该函数使用它来检索要打印的值,如以下代码所示。
With the plumbing out of the way, let’s see why these iterators are useful. If we want to print the values of all the nodes in a binary tree and all the strings in a linked list of strings, we no longer need separate functions. We can use a single common function that takes an iterator argument, which uses it to retrieve the values to print, as shown in the following code.
function print<T>(iterator: Iterator<T>): void { 1
让结果:IteratorResult<T> = iterator.next(); 2个
while (!result.done) { 3
console.log(result.value); 3
结果 = iterator.next(); 3个
}
}function print<T>(iterator: Iterator<T>): void { 1
let result: IteratorResult<T> = iterator.next(); 2
while (!result.done) { 3
console.log(result.value); 3
result = iterator.next(); 3
}
}
因为print()使用迭代器,我们可以将 aBinaryTree-Iterator<T>或 a传递给它LinkedListIterator<T>。事实上,只要我们有一个可以遍历该数据结构的迭代器,我们就可以用它来打印任何数据结构。
Because print() works with iterators, we can pass to it either a BinaryTree-Iterator<T> or a LinkedListIterator<T>. In fact, we can use it to print any data structure as long as we have an iterator that can traverse that data structure.
使用迭代器,我们可以重用更多的代码。例如,如果我们需要一种方法来确定某个值是否存在于数据结构中,我们不需要为每个数据结构实现单独的函数;我们可以简单地实现一个contains()函数,它接受一个迭代器和一个要查找的值,如下一个清单所示,然后我们可以将它与实现该Iterator<T>接口的任何迭代器一起使用(图 9.6)。
With iterators, we can reuse a lot more code. If we need a way to determine whether a certain value exists in a data structure, for example, we don’t need to implement a separate function for each data structure; we can simply implement a contains() function that takes an iterator and a value to look for, as shown in the next listing, and then we can use it with any iterator that implements the Iterator<T> interface (figure 9.6).
函数包含<T>(值:T,迭代器:迭代器<T>):布尔值{
让结果:IteratorResult<T> = iterator.next();
而(!result.done){
if (result.value == value) 返回真;
结果 = iterator.next();
}
返回假;
}function contains<T>(value: T, iterator: Iterator<T>): boolean {
let result: IteratorResult<T> = iterator.next();
while (!result.done) {
if (result.value == value) return true;
result = iterator.next();
}
return false;
}
迭代器是连接数据结构和算法的粘合剂,可以实现这种解耦。通过这种方法,如果它们之间的接口是 . ,我们可以混合和匹配具有不同功能的不同数据结构Iterator<T>。
Iterators are the glue that connects data structures and algorithms, enabling this decoupling. With this approach, we can mix and match different data structures with different functions if the interface between them is Iterator<T>.
请注意,数据结构可能具有不同的遍历。我们重点介绍了二叉树的中序遍历,但也有前序遍历和后序遍历。我们可以将所有这些遍历实现为同一二叉树上的迭代器。遍历策略和数据结构之间不一定存在一一对应关系。
Note that a data structure may have different traversals. We’ve focused on an in-order traversal of a binary tree, but there are also pre-order and post-order traversals. We can implement all these traversals as iterators over the same binary tree. A one-to-one correspondence doesn’t have to exist between traversal strategies and data structures.
迭代器非常有用,以至于大多数主流语言都为它们提供了库支持,在许多情况下,甚至还提供了特殊的语法。我们在第 6 章讨论生成器时简要地谈到了这个主题,我们将在这里展开。
Iterators are so useful that most mainstream languages provide library support for them and, in many cases, even special syntax. We briefly touched on this topic in chapter 6, when we looked at generators, and we’ll expand on it here.
我们真的不必定义IteratorResult<T>和Iterator<T>类型;TypeScript 已经预定义了它们。在 C# 中,等效接口是IEnumerator<T>,它同样支持数据结构的遍历。Java 等价物也被命名为Iterator<T>. C++ 库使用多种迭代器。我们将在第 10 章讨论迭代器类别时详细讨论这些类别。这里的关键要点是这种模式非常有用,它具有开箱即用的支持。
We didn’t really have to define the IteratorResult<T> and Iterator<T> types; TypeScript has them predefined. In C#, the equivalent interface is IEnumerator<T>, which similarly enables traversal of data structures. The Java equivalent is also named Iterator<T>. The C++ library works with several kinds of iterators. We’ll talk more about these categories in chapter 10, when we talk about iterator categories. The key takeaway here is that this pattern is so useful that it has out-of-the-box support.
迭代器实现了遍历数据结构的代码,而另一个接口让我们将类型标记为可以迭代的东西:接口Iterable<T>,定义如下。
Whereas iterators implement the code to traverse a data structure, another interface lets us mark a type as something that can be iterated over: the Iterable<T> interface, defined as follows.
接口可迭代<T> {
[Symbol.iterator](): Iterator<T>;
}interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
这[Symbol.iterator]是一些特定于 TypeScript 的语法。它只是意味着一个特殊的名字,非常像我们在整本书中用来实现名义子类型的符号技巧。该Iterable<T>接口声明了一个名为的方法[Symbol.iterator](),该方法返回一个Iterator<T>.
The [Symbol.iterator] is a bit of TypeScript-specific syntax. It just means a special name, very much like the symbol trick we used to implement nominal subtyping throughout the book. The Iterable<T> interface declares a method named [Symbol.iterator]() that returns an Iterator<T>.
让我们更新我们的LinkedListNode<T>类型并使其在下一个清单中可迭代。
Let’s update our LinkedListNode<T> type and make it iterable in the next listing.
类 LinkedListNode<T>实现 Iterable<T> {
值:T;
下一个:LinkedListNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
[Symbol.iterator](): Iterator<T> {
return new LinkedListIterator<T>(this); 1
}
}class LinkedListNode<T> implements Iterable<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
[Symbol.iterator](): Iterator<T> {
return new LinkedListIterator<T>(this); 1
}
}
[Symbol- .iterator]()我们还可以通过提供一个类似的方法来将我们的二叉树标记为可迭代的BinaryTreeIterator<T>。
We can also mark our binary tree as iterable by providing a similar [Symbol- .iterator]() method that creates a BinaryTreeIterator<T>.
Iterables 允许我们使用for ... ofTypeScript 中的语法。这种语法是用于遍历可迭代对象的所有元素的特殊语法,使我们的代码更加简洁。大多数主流语言都有一个等价物。C# 有IEnumerable<T>, IEnumerator<T>, 和foreach循环。Java 有Iterable<T>, Iterator<T>, 和for :循环。
Iterables allow us to use the for ... of syntax in TypeScript. This syntax is special syntax for iterating over all elements of an iterable and makes our code much cleaner. Most mainstream languages have an equivalent. C# has IEnumerable<T>, IEnumerator<T>, and foreach loops. Java has Iterable<T>, Iterator<T>, and for : loops.
让我们快速回顾一下下一个清单中的print()和实现,然后更新它们以使用可迭代对象和代替。 contains()for ... of
Let’s quickly review the print() and contains() implementations in the next listing and then update them to use iterables and for ... of instead.
函数打印<T>(迭代器:迭代器<T>):void {
让结果:IteratorResult<T> = iterator.next();
而(!result.done){
控制台日志(结果。值);
结果 = iterator.next();
}
}
函数包含<T>(值:T,迭代器:迭代器<T>):布尔值{
让结果:IteratorResult<T> = iterator.next();
而(!result.done){
if (result.value == value) 返回真;
结果 = iterator.next();
}
返回假;
}function print<T>(iterator: Iterator<T>): void {
let result: IteratorResult<T> = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
}
function contains<T>(value: T, iterator: Iterator<T>): boolean {
let result: IteratorResult<T> = iterator.next();
while (!result.done) {
if (result.value == value) return true;
result = iterator.next();
}
return false;
}
在下一个清单中,我们将更新函数以采用Iterable<T>参数而不是。始终可以通过调用方法从 an 中获取Iterator-<T>an 。 Iterator<T>Iterable<T>[Symbol.iterator]()
We’ll update the functions to take an Iterable<T> argument instead of an Iterator-<T> in the next listing. An Iterator<T> can always be obtained from an Iterable<T> by calling the [Symbol.iterator]() method.
function print<T>(iterable: Iterable<T>): void {
对于(可迭代的常量项){ 1
控制台日志(项目);
}
}
函数包含<T>(值:T,可迭代:Iterable<T>):布尔值{
对于(可迭代的常量项){ 2
如果(项目==值)返回真;
}
返回假;
}function print<T>(iterable: Iterable<T>): void {
for (const item of iterable) { 1
console.log(item);
}
}
function contains<T>(value: T, iterable: Iterable<T>): boolean {
for (const item of iterable) { 2
if (item == value) return true;
}
return false;
}
如我们所见,代码更加简洁。Iterator<T>不用使用and手动遍历我们的数据结构next(),我们可以使用一个使用for...of.
As we can see, the code is much more succinct. Instead of iterating over our data structures manually, using an Iterator<T> and next(), we can do it with a one-liner that uses for...of.
现在让我们看看如何简化我们的迭代器代码。我们说过我们的有序二叉树遍历是低效的,因为它在返回之前对所有节点进行排队。一个更有效的解决方案是遍历树而不对所有节点进行排队,但实现会变得有点复杂。以下是我们目前使用的实现。
Now let’s see how we can simplify our iterator code. We said that our in-order binary tree traversal is inefficient, as it queues all the nodes before returning them. A more efficient solution would traverse the tree without queuing all nodes, but the implementation would get a bit more complex. Following is the implementation we’ve used so far.
类 BinaryTreeIterator<T> 实现 Iterator<T> {
私有值:T[];
私有根:BinaryTreeNode<T>;
构造函数(根:BinaryTreeNode<T>){
this.values = [];
this.root = root;
这个.inOrder(根);
}
下一个():迭代器结果<T> {
常量结果:T | undefined = this.values.shift();
如果(!结果){
return { done: true, value: this.root.value };
}
返回{完成:假,值:结果};
}
private inOrder(node: BinaryTreeNode<T>): void {
如果(node.left!=未定义){
这个.inOrder(node.left);
}
this.values.push(node.value);
如果(node.right!=未定义){
this.inOrder(node.right);
}
}
}class BinaryTreeIterator<T> implements Iterator<T> {
private values: T[];
private root: BinaryTreeNode<T>;
constructor(root: BinaryTreeNode<T>) {
this.values = [];
this.root = root;
this.inOrder(root);
}
next(): IteratorResult<T> {
const result: T | undefined = this.values.shift();
if (!result) {
return { done: true, value: this.root.value };
}
return { done: false, value: result };
}
private inOrder(node: BinaryTreeNode<T>): void {
if (node.left != undefined) {
this.inOrder(node.left);
}
this.values.push(node.value);
if (node.right != undefined) {
this.inOrder(node.right);
}
}
}
我们能做的就是用生成器替换这段代码。(我们在第 6 章中简要讨论了生成器。)生成器是一个可恢复的函数,它使用一条yield语句返回,并在再次调用时从它停止的地方恢复执行。TypeScript 中的生成器返回一个IterableIterator<T>,它只是我们了解的两个接口的组合:Iterable<T>和Iterator<T>。实现两者的对象可以“手动”迭代,next()但也可以在语句中使用for...of。
What we can do is replace this code with a generator. (We briefly talked about generators in chapter 6.) A generator is a resumable function that returns using a yield statement and, when called again, resumes execution from where it left off. Generators in TypeScript return an IterableIterator<T>, which is simply a combination of the two interfaces we’ve learned about: Iterable<T> and Iterator<T>. An object that implements both can be iterated over “manually” with next() but can also be used in a for...of statement.
让我们在代码清单 9.30中将我们的二叉树遍历重新实现为一个生成器。使用生成器,我们可以递归地实现遍历并不断产生值,直到我们遍历整个数据结构(图 9.7)。
Let’s reimplement our binary tree traversal as a generator in listing 9.30. With generators, we can implement traversal recursively and keep yielding values until we’ve gone over the whole data structure (figure 9.7).
函数* inOrderIterator<T>(根:BinaryTreeNode<T>): 1
IterableIterator<T> {
如果(根。左){
for (inOrderIterator(root.left) 的常量值) {
屈服值; 2个
}
}
产生根值; 3个
如果(root.right){
for (inOrderIterator(root.right) 的常量值) {
屈服值; 4个
}
}
}function* inOrderIterator<T>(root: BinaryTreeNode<T>): 1
IterableIterator<T> {
if (root.left) {
for (const value of inOrderIterator(root.left)) {
yield value; 2
}
}
yield root.value; 3
if (root.right) {
for (const value of inOrderIterator(root.right)) {
yield value; 4
}
}
}
这个实现要简洁得多。注意inOrderIterator()是递归的。在每个级别,值都“向上”产生,直到它们传播到原始调用者。
This implementation is much more succinct. Note that inOrderIterator() is recursive. At each level, values are yielded “up” until they propagate to the original caller.
同样,我们可以用一个生成器遍历我们的链表,简化逻辑。我们最初的实现看起来像下面的清单。
Similarly, we can traverse our linked list with a generator, simplifying the logic. Our original implementation looked like the following listing.
类 LinkedListIterator<T> 实现 Iterator<T> {
私有头:LinkedListNode<T>;
私人电流:LinkedListNode<T> | 不明确的;
构造函数(头:LinkedListNode<T>){
this.head = 头;
this.current = head;
}
下一个():迭代器结果<T> {
如果(!this.current){
return { done: true, value: this.head.value };
}
常量结果:T = this.current.value;
this.current = this.current.next;
返回{完成:假,值:结果};
}
}class LinkedListIterator<T> implements Iterator<T> {
private head: LinkedListNode<T>;
private current: LinkedListNode<T> | undefined;
constructor(head: LinkedListNode<T>) {
this.head = head;
this.current = head;
}
next(): IteratorResult<T> {
if (!this.current) {
return { done: true, value: this.head.value };
}
const result: T = this.current.value;
this.current = this.current.next;
return { done: false, value: result };
}
}
我们可以将其替换为另一个在遍历列表时产生值的生成器,如以下清单所示。
We can replace this with another generator that yields values as it traverses the list, as shown in the following listing.
函数* linkedListIterator<T>(头:LinkedListNode<T>):
IterableIterator<T> {
让当前:LinkedListNode<T> | 未定义=头;
而(当前){
产生当前值; 1个
当前=当前.下一个;
}
}function* linkedListIterator<T>(head: LinkedListNode<T>):
IterableIterator<T> {
let current: LinkedListNode<T> | undefined = head;
while (current) {
yield current.value; 1
current = current.next;
}
}
编译器将其转换为一个迭代器,该迭代器提供IteratorResult<T>每个 的值yield。当函数到达末尾并退出(没有产生值)时,返回 一个设置为 的IteratorResult<T>final 。donetrue
The compiler translates this into an iterator that provides IteratorResult<T> values from each yield. When the function reaches the end and exits (without yielding a value), a final IteratorResult<T> with done set to true is returned.
最后一步是将这些生成器作为[Symbol.iterator](). 让我们看看链表的最终版本是什么样的。
The final step is plugging these generators into the data structures themselves as implementations of [Symbol.iterator](). Let’s see what our final version of the linked list looks like.
类 LinkedListNode<T> 实现 Iterable<T> {
值:T;
下一个:LinkedListNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
[Symbol.iterator](): Iterator<T> {
返回链接列表迭代器(这个); 1个
}
}class LinkedListNode<T> implements Iterable<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
[Symbol.iterator](): Iterator<T> {
return linkedListIterator(this); 1
}
}
这是有效的,因为生成器返回一个IterableIterator<T>. 有时我们需要 anIterable<T>以便我们可以在循环中嵌入对生成器的调用for...of(例如,for (const value of linkedListIterator(...))。有时我们需要一个Iterator<T>代替,如前面的示例,因此我们可以for...of在数据结构本身的实例上使用循环。
This works because the generator returns an IterableIterator<T>. Sometimes we want an Iterable<T> so we can embed a call to the generator inside a for...of loop (for example, for (const value of linkedListIterator(...)). Sometimes we want an Iterator<T> instead, as in the preceding example, so we can use a for...of loop on an instance of the data structure itself.
我们从处理数据形状的几个通用数据结构开始,不管数据是什么。我们看到这种抽象很强大。但是,如果我们编写代码来遍历每个数据结构,每当我们想要对其应用操作时,例如print()or contains(),我们最终会得到每个函数的多个版本。
We started with a couple of generic data structures that took care of the shape of the data, regardless of what that data was. We saw that this abstraction is powerful. But if we write code to traverse each data structure whenever we want to apply an operation over it, such as print() or contains(), we end up with multiple versions of each function.
Enter Iterator<T>,一个通过使用next(). 这个接口允许我们编写一个单一版本的print()和一个单一版本的contains(),都在迭代器上运行。
Enter Iterator<T>, an interface that decouples the shape of the data from the functions by providing a unified traversal interface using next(). This interface allows us to write a single version of print() and a single version of contains(), both operating on iterators.
但是,通过调用next()和检查进行迭代done仍然很麻烦。原来Iterable<T>是一个声明[Symbol.iterator]()方法的接口。我们可以使用此方法来获取迭代器。Iterable<T>更好的是,我们可以在声明中添加一个for...of。这种语法不仅更简洁,而且我们永远不必显式地处理迭代器,因为在循环的每次迭代中,我们都会得到实际的元素。
Iterating by calling next() and checking done is still cumbersome, though. Turns out Iterable<T> is an interface that declares a [Symbol.iterator]() method. We can use this method to get an iterator. Better yet, we can put an Iterable<T> in a for...of statement. Not only is this syntax cleaner, but we also never have to deal with the iterator explicitly, as on each iteration of the loop, we get the actual element.
最后,我们看到如果我们使用一个在遍历数据结构时产生值的生成器,我们可以简化遍历代码。生成器返回一个Iterable-Iterator<T>,因此我们可以直接在循环内使用它们for...of或实现数据结构的Iterable<T>接口。
Finally, we saw that we can simplify the traversal code if we use a generator that yields values as it traverses the data structure. Generators return an Iterable-Iterator<T>, so we can use them both directly inside for...of loops or to implement a data structure’s Iterable<T> interface.
如前所述,大多数主流编程语言都有一个等效的特殊类型,可以实现for遍历元素的循环。至于生成器,虽然 Java 缺少内置yield语句,但 C# 支持它们,使用与 TypeScript 非常相似的语法。
As mentioned earlier, most mainstream programming languages have an equivalent special type that enables a for loop that traverses over elements. As for generators, although Java lacks a built-in yield statement, C# supports them, using a syntax very similar to TypeScript’s.
通常,在定义数据结构时,确保它实现了Iterable<T>. 避免编写嵌入特定数据结构遍历的函数;相反,让它们与迭代器一起工作,以便可以对不同的数据结构重用相同的逻辑。yield在实现遍历逻辑时 考虑一下,因为它通常会使代码更干净、更简洁。
In general, when defining a data structure, make sure that it implements Iterable<T>. Avoid writing functions that embed traversal of one particular data structure; rather, have them work with iterators so that the same logic can be reused with different data structures. Consider yield when implementing the traversal logic, as it usually makes code cleaner and more concise.
不幸的是,我们必须使用IteratorResult<T>. 作为返回类型next()。这就是在 TypeScript 中开箱即用地定义接口的方式。这违反了我们在第 3 章中概述的从函数返回结果或错误而不是两者的原则。IteratorResult<T>包含一个boolean属性done和一个value类型的属性T。当迭代器遍历整个列表时,它返回doneastrue但还需要返回一些东西 for value。这value必须是一些默认值,因为它是强制性的,但数据结构已被完全遍历。调用代码绝不意味着使用 valueif doneis true。不幸的是,没有办法强制执行此规则。
It’s unfortunate that we have to use IteratorResult<T> as the return type of next(). This is how the interface is defined out of the box in TypeScript. It goes against the principle we outlined in chapter 3 to return result or error from a function as opposed to both. IteratorResult<T> contains a boolean property done and a value property of type T. When the iterator has traversed the whole list, it returns done as true but also needs to return something for value. This value must be some default, as it is mandatory, but the data structure was fully traversed. Calling code is never meant to use value if done is true. Unfortunately, there is no way to enforce this rule.
更好的合同是总和类型,例如Optional<T>或T | undefined。T只要值可用, 这就会返回s ,然后在遍历完成时返回任何内容。
A better contract would be a sum type such as Optional<T> or T | undefined. This will return Ts as long as values are available and then nothing when traversal is finished.
为通用二叉树实现预序遍历。前序遍历先是父树,再是左子树,再是右子树。尝试用生成器实现它。
Implement a pre-order traversal for a generic binary tree. Pre-order traversal is parent first, followed by left subtree and then right subtree. Try implementing it with a generator.
实现一个向后(从后到前)迭代数组的函数。
Implement a function that iterates over an array backward (from back to front).
在最后一节中,我们将了解迭代器的一个非常有趣的方面:它们不一定是有限的。在下一个清单中,让我们实现一个生成无限随机数流的函数。我们将调用它generateRandomNumbers()并让它从无限循环中产生这些数字。
In this last section, we will look at a very interesting aspect of iterators: the fact that they don’t necessarily have to be finite. In the next listing, let’s implement a function that generates an infinite stream of random numbers. We’ll call it generateRandomNumbers() and have it yield these numbers from an infinite loop.
函数* generateRandomNumbers(): IterableIterator<number> {
while (true) { 1
yield Math.random(); 2个
}
}function* generateRandomNumbers(): IterableIterator<number> {
while (true) { 1
yield Math.random(); 2
}
}
我们可以调用此函数来获取 an IterableIterator<T>,然后next()多次调用它来获取随机数,如以下清单所示。
We can call this function to get an IterableIterator<T> and then call next() on it a few times to get random numbers, as shown in the following listing.
让 iter: IterableIterator<number> = generateRandomNumbers(); console.log(iter.next().value); console.log(iter.next().value); console.log(iter.next().value);
let iter: IterableIterator<number> = generateRandomNumbers(); console.log(iter.next().value); console.log(iter.next().value); console.log(iter.next().value);
现实生活中有很多无限数据流的例子:从键盘读取字符、通过网络连接接收数据、收集传感器数据等等。我们可以使用管道来处理这些数据。
There are many examples of infinite streams of data in real life: reading characters from the keyboard, receiving data over a network connection, collecting sensor data, and so on. We can process such data by using pipelines.
处理管道的组件是将迭代器作为参数、进行一些处理并返回迭代器的函数。这些功能可以链接在一起以在数据到达时对其进行处理。这种模式在函数式编程语言中很常见,也是反应式编程的基础。
The components of processing pipelines are functions that take an iterator as argument, do some processing, and return an iterator. Such functions can be chained together to process data as it arrives. This pattern is common in functional programming languages and the basis of reactive programming.
例如,让我们实现一个square()函数,对其输入迭代器的所有数字进行平方。我们可以使用一个生成器轻松地做到这一点,该生成器接受一个Iterable- <number>参数并生成其值的平方,如清单 9.36所示。请注意,我们不需要IterableIterator<number>as 输入——只需一个Iterable- <number>——但传入一个就可以了,因为 anIterableIterator<number>也满足Iterable<number>接口。
As an example, let’s implement a square() function that squares all numbers of its input iterator. We can do this easily with a generator that takes an Iterable- <number> argument and yields squares of its values, as shown in listing 9.36. Note that we don’t need an IterableIterator<number> as input—just an Iterable- <number>—but passing one in will work, as an IterableIterator<number> also satisfies the Iterable<number> interface.
function* square(iter: Iterable<number>): 1
IterableIterator<number> { 1
for (iter 的常量值) {
屈服值** 2;
}
}function* square(iter: Iterable<number>): 1
IterableIterator<number> { 1
for (const value of iter) {
yield value ** 2;
}
}
处理管道中的一个常见函数是take(),该函数获取n其输入迭代器的第一个元素并返回它们,丢弃其余元素,如以下代码所示。
A common function in processing pipeline is take(), a function that takes the first n elements of its input iterator and returns them, discarding the rest, as shown in the following code.
函数* take<T>(iter: Iterable<T>, n: number):
IterableIterator<T> {
for (iter 的常量值) {
如果 (n-- <= 0) 返回; 1个
屈服值; 2个
}
}function* take<T>(iter: Iterable<T>, n: number):
IterableIterator<T> {
for (const value of iter) {
if (n-- <= 0) return; 1
yield value; 2
}
}
现在让我们在清单 9.38中创建一个管道,它对无限流中的数字进行平方并获取前五个结果,我们将其打印到控制台(图 9.8)。
Now let’s create a pipeline in listing 9.38 that squares numbers from an infinite stream and takes the first five results, which we print to the console (figure 9.8).
常量值:IterableIterator<number> =
采取(广场(generateRandomNumbers()),5); 1个
对于(值的常量值){
控制台日志(值);
}const values: IterableIterator<number> =
take(square(generateRandomNumbers()), 5); 1
for (const value of values) {
console.log(value);
}
迭代器是创建此类管道的关键,因为它们支持对值进行逐一处理。同样重要的是要了解这些管道是惰性评估的。在此清单的示例中,values是一个IterableIterator<number>. 尽管它是通过调用我们的管道创建的,但尚未执行任何代码。只有当我们开始在for...of循环中使用值时,值才会开始流动。
Iterators are the key to creating this type of pipeline, as they enable one-by-one processing of values. It’s also important to understand that these pipelines are evaluated lazily. In our example in this listing, values is an IterableIterator<number>. Even though it is created by calling our pipeline, none of the code is executed yet. Only when we start consuming values in the for...of loop do values start flowing through.
在循环的一次迭代中,在迭代器next()上调用values,它调用take(). take()需要一个值,因此它依次调用square(). 同样,square()需要一个值来平方,所以它调用generateRandomNumbers(). generateRandom-Numbers()产生一个随机值到square(),将它平方并产生它到take()。take()将它产生到循环中,在那里它被打印到控制台。
In one iteration of the loop, next() is called on the values iterator, which invokes take(). take() needs a value, so it in turn calls square(). Similarly, square() needs a value to square, so it calls generateRandomNumbers(). generateRandom-Numbers() yields a random value to square(), which squares it and yields it to take(). take() yields it to the loop, where it is printed to the console.
因为管道是惰性评估的,所以我们可以使用无限生成器,例如generateRandomNumbers(). 我们将在第 10 章更深入地介绍算法。
Because pipelines are evaluated lazily, we can work with infinite generators, such as generateRandomNumbers(). We’ll cover algorithms in more depth in chapter 10.
drop()是另一个常用函数。此函数与 相反take(),因为它丢弃迭代器的第一个n元素并返回其余元素。实施drop()。
drop() is another common function. This function is the opposite of take(), as it discards the first n elements of an iterator and returns the rest. Implement drop().
创建一个管道,给定一个迭代器,返回第六、第七、第八、第九和第十个元素。drop()提示:这可以通过和的组合来完成take()。
Create a pipeline that, given an iterator, returns the sixth, seventh, eighth, ninth, and tenth elements. Hint: this can be done with a combination of drop() and take().
现在我们已经介绍了通用数据结构,第 10 章将着眼于编程的其他主要成分:算法。
Now that we’ve covered generic data structures, chapter 10 looks at the other main ingredients of programing: algorithms.
一个可能的实现:
类框 <T> { 只读值:T; 构造函数(值:T){ this.value = 值; } }
A possible implementation:
class Box<T> { readonly value: T; constructor(value: T) { this.value = value; } }
一个可能的实现:
函数 unbox<T>(boxed: Box<T>): T { 返回装箱值; }
A possible implementation:
function unbox<T>(boxed: Box<T>): T { return boxed.value; }
一个由数组支持的可能实现(在 JavaScript 中,数组自带pop()和push()开箱即用):
类堆栈<T> { 私有值:T[] = []; 公共推送(值:T){ this.values.push(值); } 公共 pop(): T { 如果(this.values.length == 0)抛出错误(); 返回 this.values.pop(); } 公众偷看():T { 如果(this.values.length == 0)抛出错误(); 返回 this.values[this.values.length - 1]; } }
A possible implementation backed by an array (in JavaScript, arrays come with pop() and push() out of the box):
class Stack<T> { private values: T[] = []; public push(value: T) { this.values.push(value); } public pop(): T { if (this.values.length == 0) throw Error(); return this.values.pop(); } public peek(): T { if (this.values.length == 0) throw Error(); return this.values[this.values.length - 1]; } }
一个可能的实现:
类对 <T, U> { 只读优先:T; 只读第二个:U; 构造函数(第一:T,第二:U){ this.first = 第一; this.second = 第二个; } }
A possible implementation:
class Pair<T, U> { readonly first: T; readonly second: U; constructor(first: T, second: U) { this.first = first; this.second = second; } }
此实现与有序实现非常相似;我们只是root.value在产生左子树之前产生:
函数* preOrderIterator<T>(根:BinaryTreeNode<T>): IterableIterator<T> { 产生根值; 如果(根。左){ for (preOrderIterator(root.left) 的常量值) { 屈服值; } } 如果(root.right){ for (preOrderIterator(root.right) 的常量值) { 屈服值; } } }
This implementation is very similar to the in-order one; we just yield root.value before we yield the left subtree:
function* preOrderIterator<T>(root: BinaryTreeNode<T>): IterableIterator<T> { yield root.value; if (root.left) { for (const value of preOrderIterator(root.left)) { yield value; } } if (root.right) { for (const value of preOrderIterator(root.right)) { yield value; } } }
此实现确实使用for循环向后遍历数组,因此调用者不必这样做。
函数* backwardsArrayIterator<T>(array: T[]): IterableIterator<T> { for (let i = array.length - 1; i >= 0; i--) { 产量数组[i]; } }
This implementation does use a for loop to traverse the array backward so callers don’t have to.
function* backwardsArrayIterator<T>(array: T[]): IterableIterator<T> { for (let i = array.length - 1; i >= 0; i--) { yield array[i]; } }
一个可能的实现:
函数* drop<T>(iter: Iterable<T>, n: number): IterableIterator<T> { for (iter 的常量值) { 如果 (n-- > 0) 继续; 屈服值; } }
A possible implementation:
function* drop<T>(iter: Iterable<T>, n: number): IterableIterator<T> { for (const value of iter) { if (n-- > 0) continue; yield value; } }
我们可以定义count()一个计数器,它产生从 1 开始的数字并继续运行。就其产生的价值流而言,我们drop()先看前五个,然后take()再看后五个:
函数* count(): IterableIterator<number> { 让 n: 数字 = 0; 而(真){ n++; 产量 n; } } for (let value of take(drop(count(), 5), 5)) { 控制台日志(值);
We can define count(), a counter that yields numbers starting from 1 and keeps going. Taking the stream of value it produces, we drop() the first five and then take() the next five:
function* count(): IterableIterator<number> { let n: number = 0; while (true) { n++; yield n; } } for (let value of take(drop(count(), 5), 5)) { console.log(value);
本章涵盖
This chapter covers
本章都是关于通用算法的——适用于各种数据类型和数据结构的可重用算法。
This chapter is all about generic algorithms—reusable algorithms that work on various data types and data structures.
在第 5 章讨论高阶函数时,我们分别查看了map()、filter()和的一个版本。这些函数对数组进行操作,但正如我们在前面的章节中看到的那样,迭代器提供了对任何数据结构的良好抽象。我们将从实现这三种与迭代器一起工作的算法的通用版本开始,这样我们就可以将它们应用于二叉树、列表、数组和任何其他可迭代的数据结构。 reduce()
We looked at one version each of map(), filter(), and reduce() in chapter 5, when we discussed higher-order functions. Those functions operated on arrays, but as we saw in the previous chapters, iterators provide a nice abstraction over any data structure. We’ll start by implementing generic versions of these three algorithms that work with iterators, so we can apply them to binary trees, lists, arrays, and any other iterable data structures.
map(), filter(), 和reduce()不是唯一的。我们将讨论可用于大多数现代编程语言的其他通用算法和算法库。我们将看到为什么我们应该用对库算法的调用来替换大多数循环。我们还将简要讨论流畅的 API 以及算法的用户友好界面是什么样的。
map(), filter(), and reduce() are not unique. We’ll talk about other generic algorithms and algorithm libraries that are available to most modern programming languages. We’ll see why we should replace most loops with calls to library algorithms. We’ll also briefly talk about fluent APIs and what a user-friendly interface for algorithms looks like.
接下来,我们将讨论类型参数约束;通用数据结构和算法可以指定它们需要在其参数类型上可用的某些功能。这种类型的专业化允许通用的数据结构和算法,这些数据结构和算法并非在任何地方都适用;它们不太笼统。
Next, we’ll go over type parameter constraints; generic data structures and algorithms can specify certain features they need available on their parameter types. This type of specialization allows for generic data structures and algorithms that don’t work everywhere; they are somewhat less general.
我们将重点关注迭代器并讨论所有不同类别的迭代器。更专业的迭代器支持更高效的算法。权衡是并非所有数据结构都可以支持专门的迭代器。
We’ll zoom in on iterators and talk about all the different categories of iterators. More specialized iterators enable more efficient algorithms. The trade-off is that not all data structures can support specialized iterators.
最后,我们将快速浏览一下自适应算法。此类算法为具有较少功能的迭代器提供更通用、效率较低的实现,并为具有更多功能的迭代器提供更高效、不太通用的实现。
Finally, we’ll take a quick look at adaptive algorithms. Such algorithms provide more general, less efficient implementations for iterators with fewer capabilities and more efficient, less general implementations for iterators with more capabilities.
在第 5 章中,我们讨论了map()、filter()和reduce(),并研究了它们中每一个的可能实现。这些算法是高阶函数,因为它们每个都将另一个函数作为参数并将其应用于序列。
In chapter 5, we talked about map(), filter(), and reduce(), and looked at a possible implementation of each of them. These algorithms are higher-order functions, as they each take another function as an argument and apply it over a sequence.
map()将函数应用于序列的每个元素并返回结果。filter()对每个元素应用过滤函数,并仅返回该函数返回的元素true。reduce()使用给定函数组合序列中的所有值,并返回单个值作为结果。
map() applies the function to each element of the sequence and returns the results. filter() applies a filtering function to each element and returns only the elements for which that function returns true. reduce() combines all the values in the sequence, using the given function, and returns a single value as the result.
我们在第 5 章中的实现使用了泛型类型参数T,序列表示为 的数组T。
Our implementation in chapter 5 used a generic type parameter T, and the sequences were represented as arrays of T.
让我们来看看我们是如何实现的map()。我们使用了两个类型参数:T和U。该函数将一个T值数组作为第一个参数,并将一个从T到的函数U作为第二个参数。它返回一个值数组U,如下一个清单所示。
Let’s take a look at how we implemented map(). We used two type parameters: T and U. The function takes an array of T values as the first argument and a function from T to U as the second argument. It returns an array of U values, as shown in the next listing.
function map<T, U>(items: T[], func: (item: T) => U): U[] { 1 let
result: U[] = []; 2个
对于(项目的常量项目){
结果.推送(功能(项目)); 3个
}
返回结果; 4
}function map<T, U>(items: T[], func: (item: T) => U): U[] { 1
let result: U[] = []; 2
for (const item of items) {
result.push(func(item)); 3
}
return result; 4
}
现在我们了解了迭代器和生成器,让我们在下一个清单中看看我们如何实现以map()在 any 上工作Iterable<T>,而不仅仅是数组。
Now that we know about iterators and generators, let’s see in the next listing how we can implement map() to work on any Iterable<T>, not only arrays.
function * map<T, U>( iter: Iterable<T> , func: (item: T) => U): 1
IterableIterator<U> { 2
for (iter 的常量值) {
收益函数(值); 3个
}
}function* map<T, U>(iter: Iterable<T>, func: (item: T) => U): 1
IterableIterator<U> { 2
for (const value of iter) {
yield func(value); 3
}
}
虽然最初的实现仅限于数组,但这个实现适用于提供迭代器的任何数据结构。不仅如此,它还更加简洁。
Whereas the original implementation was restricted to arrays, this one works with any data structure that provides an iterator. Not only that, but it is also more concise.
让我们对 做同样的事情filter()。我们最初的实现需要一个类型数组T和一个谓词。提醒一下,谓词是一种函数,它接受一个某种类型的参数并返回一个boolean. 如果函数返回true该值,我们说该值满足谓词。
Let’s do the same for filter(). Our original implementation expected an array of type T and a predicate. As a reminder, a predicate is a function that takes one argument of some type and returns a boolean. We say that a value satisfies the predicate if the function returns true for that value.
function filter<T>(items: T[], pred: (item: T) => boolean): T[] { 1
让结果:T[] = [];
对于(项目的常量项目){
如果(预测(项目)){ 2
结果.推送(项目);
}
}
返回结果;
}function filter<T>(items: T[], pred: (item: T) => boolean): T[] { 1
let result: T[] = [];
for (const item of items) {
if (pred(item)) { 2
result.push(item);
}
}
return result;
}
正如我们对 map() 所做的那样,我们将使用 anIterable<T>而不是数组并将此可迭代对象实现为生成器,该生成器会生成满足谓词的值,如以下清单所示。
Just as we did with map(), we are going to use an Iterable<T> instead of an array and implement this iterable as a generator that yields values that satisfy the predicate, as shown in the following listing.
函数* filter<T>(iter: Iterable<T> , pred: (item: T) => boolean): 1
IterableIterator<T> { 2
for (iter 的常量值) {
如果(预测值(值)){
屈服值; 3个
}
}
}function* filter<T>(iter: Iterable<T>, pred: (item: T) => boolean): 1
IterableIterator<T> { 2
for (const value of iter) {
if (pred(value)) {
yield value; 3
}
}
}
我们再次以一个更短的实现结束,它不仅可以处理数组。最后,让我们更新一下reduce()。
We again end up with a shorter implementation that works with more than arrays. Finally, let’s update reduce().
我们最初的实现reduce()期望一个 array of T,一个 type 的初始值T(如果数组为空)和一个 operation op()。该操作是一个函数,它接受两个类型的值T并返回一个类型的值T。reduce()将操作应用于初始值和数组的第一个元素,存储结果,将操作应用于结果和数组的下一个元素,依此类推。
Our original implementation of reduce() expected an array of T, an initial value of type T (in case the array is empty), and an operation op(). The operation is a function that takes two values of type T and returns a value of type T. reduce() applies the operation to the initial value and the first element of the array, stores the result, applies the operation to the result and the next element of the array, and so on.
function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T { 1
让结果:T = init;
对于(项目的常量项目){
结果 = 操作(结果,项目); 2个
}
返回结果;
}function reduce<T>(items: T[], init: T, op: (x: T, y: T) => T): T { 1
let result: T = init;
for (const item of items) {
result = op(result, item); 2
}
return result;
}
我们可以重写它以使用一个Iterable<T>instead ,以便它适用于任何序列,如以下代码所示。在这种情况下,我们不需要生成器。与前面两个函数不同,reduce()返回的不是元素序列,而是单个值。
We can rewrite this to use an Iterable<T> instead so that it works with any sequence, as shown in the following code. In this case, we don’t need a generator. Unlike the previous two functions, reduce() does not return a sequence of elements, but a single value.
函数 reduce<T>( iter: Iterable<T> , init: T, 1
op: (x: T, y: T) => T): T {
让结果:T = init;
for (iter 的常量值) {
结果=操作(结果,价值);
}
返回结果;
}function reduce<T>(iter: Iterable<T>, init: T, 1
op: (x: T, y: T) => T): T {
let result: T = init;
for (const value of iter) {
result = op(result, value);
}
return result;
}
其余的实现没有改变。
The rest of the implementation is unchanged.
让我们看看我们如何将这些算法组合成一个管道,该管道仅从二叉树中获取偶数并将它们相加。我们将使用第 9 章BinaryTreeNode<T>中的顺序遍历,并将其与偶数过滤器和使用加法链接起来作为操作。 reduce()
Let’s see how we can combine these algorithms into a pipeline that takes only the even numbers from a binary tree and sums them up. We’ll use our BinaryTreeNode<T> from chapter 9, with its in-order traversal, and chain this with an even number filter and a reduce() using addition as the operation.
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1); 1个
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
常量结果:数字 =
减少(
筛选(
inOrderIterator(root), 2
(value) => value % 2 == 0), 3
0, (x, y) => x + y); 4个
控制台日志(结果);let root: BinaryTreeNode<number> = new BinaryTreeNode(1); 1
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
const result: number =
reduce(
filter(
inOrderIterator(root), 2
(value) => value % 2 == 0), 3
0, (x, y) => x + y); 4
console.log(result);
这个例子应该加强泛型的强大程度。我们不必实现一个新的功能来遍历二叉树并求和偶数,我们只是简单地组装了一个为这个场景定制的处理管道。
This example should reinforce how powerful generics are. Instead of having to implement a new function to traverse the binary tree and sum up even numbers, we simply put together a processing pipeline customized for this scenario.
通过连接所有非空字符串来构建一个处理可迭代类型的管道string。
Build a pipeline that processes an iterable of type string by concatenating all nonempty strings.
number通过选择所有奇数并对它们进行平方来 构建一个处理可迭代类型的管道。
Build a pipeline that processes an iterable of type number by selecting all odd numbers and squaring them.
我们在第 9 章中查看了map()、filter()和reduce(),并且也提到了。许多其他算法通常用于流水线。让我们列出其中的一些。我们不会查看实现——只描述除了他们期望的可迭代对象之外还有哪些参数以及他们如何处理数据。我们还将提到算法可能出现的一些同义词: take()
We looked at map(), filter(), and reduce(), and also mentioned take() in chapter 9. Many other algorithms are commonly used in pipelines. Let’s list a few of them. We will not look at the implementations—just describe what arguments besides the iterable they expect and how they process the data. We’ll also mention some synonyms under which the algorithm might appear:
还有更多用于排序、反转、拆分和连接序列的算法。好消息是,由于这些算法非常有用且普遍适用,我们不需要实施它们。大多数语言都有提供这些算法和更多算法的库。JavaScript 有underscore.jspackage 和lodashpackage,它们都提供了大量这样的算法。(在撰写本文时,这些库不支持迭代器——仅支持 JavaScript 内置数组和对象类型。)在 Java 中,它们位于包中java.util.stream。在 C# 中,它们位于System.Linq命名空间中。在 C++ 中,它们位于<algorithm>标准库头文件中。
There are many more algorithms for sorting, reversing, splitting, and concatenating sequences. The good news is that because these algorithms are so useful and generally applicable, we don’t need to implement them. Most languages have libraries that provide these algorithms and more. JavaScript has the underscore.js package and the lodash package, both of which provide a plethora of such algorithms. (At the time of writing, these libraries don’t support iterators—only the JavaScript built-in array and object types.) In Java, they are in the java.util.stream package. In C#, they are in the System.Linq namespace. In C++, they are in the <algorithm> standard library header.
您可能会感到惊讶,一个好的经验法则是在您发现自己编写循环时检查库算法或管道是否可以完成这项工作。通常,我们编写循环来处理一个序列,这正是我们讨论的算法所做的。
You may be surprised that a good rule of thumb is to check, whenever you find yourself writing a loop, whether a library algorithm or a pipeline can do the job. Usually, we write loops to process a sequence, which is exactly what the algorithms we talked about do.
在循环中更喜欢库算法而不是自定义代码的原因是出错的机会更少。库算法经过反复试验和有效实施,我们最终得到的代码更容易理解,因为操作被拼写出来了。
The reason to prefer library algorithms to custom code in loops is that there is less opportunity for mistakes. Library algorithms are tried and tested and implemented efficiently, and the code we end up with is easier to understand, as the operations are spelled out.
我们在本书中研究了一些实现,以更好地理解事物的底层工作原理,但您很少需要自己实现算法。如果您最终遇到可用算法无法解决的问题,请考虑对您的解决方案进行通用的、可重用的实现,而不是一次性的特定实现。
We’ve looked at a few implementations throughout this book to get a better understanding of how things work under the hood, but you’ll rarely need to implement an algorithm yourself. If you do end up with a problem that the available algorithms can’t solve, consider making a generic, reusable implementation of your solution rather than a one-off specific implementation.
大多数库还提供流畅的 API 以将算法链接到管道中。Fluent API 是基于方法链的 API,使代码更易于阅读。要了解流畅 API 和非流畅 API 之间的区别,让我们再看一下 10.1.4 节中的过滤/归约管道。
Most libraries also provide a fluent API to chain algorithms into a pipeline. Fluent APIs are APIs based on method chaining, making the code much easier to read. To see the difference between a fluent and a nonfluent API, let’s take another look at the filter/reduce pipeline from section 10.1.4.
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
常量结果:数字 =
减少(
筛选(
inOrderBinaryTreeIterator(root),
(值) => 值 % 2 == 0),
0, (x, y) => x + y);
控制台日志(结果);let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
const result: number =
reduce(
filter(
inOrderBinaryTreeIterator(root),
(value) => value % 2 == 0),
0, (x, y) => x + y);
console.log(result);
即使我们filter()先应用然后将结果传递给reduce(),但如果我们从左到右阅读代码,我们会看到reduce()之前filter()。也很难理解哪些参数与管道中的哪个函数一起使用。流畅的 API 使代码更易于阅读。
Even though we apply filter() first and then pass the result to reduce(), if we read the code from left to right, we see reduce() before filter(). It’s also a bit hard to make sense of which arguments go with which function in the pipeline. Fluent APIs make the code much easier to read.
目前,我们所有的算法都将可迭代对象作为第一个参数并返回一个迭代器。我们可以使用面向对象编程来改进我们的 API。我们可以将所有算法放在一个包装可迭代对象的类中。然后我们可以调用任何可迭代对象而无需显式提供可迭代对象作为第一个参数;可迭代对象是一个成员类的。让我们为map(),执行此操作filter(),并将reduce()它们分组到一个包装可迭代对象的新FluentIterable<T>类中,如下一个清单所示。
Currently, all our algorithms take an iterable as the first argument and return an iterator. We can use object-oriented programming to improve our API. We can put all our algorithms in a class that wraps an iterable. Then we can call any of the iterables without explicitly providing an iterable as the first argument; the iterable is a member of the class. Let’s do this for map(), filter(), and reduce() by grouping them into a new FluentIterable<T> class wrapping an iterable, as shown in the next listing.
类 FluentIterable<T> { 1
iter: Iterable<T>; 1个
构造函数(iter: Iterable<T>) {
这个.iter = iter;
}
*map<U>(func: (item: T) => U): IterableIterator<U> { 2
for (this.iter 的常量值) {
收益函数(值);
}
}
*filter(pred: (item: T) => boolean): IterableIterator<T> { 2
for (this.iter 的常量值) {
如果(预测值(值)){
屈服值;
}
}
}
减少(初始化:T,操作:(x:T,y:T)=> T):T { 2
让结果:T = init;
for (this.iter 的常量值) {
结果=操作(结果,价值);
}
返回结果;
}
}class FluentIterable<T> { 1
iter: Iterable<T>; 1
constructor(iter: Iterable<T>) {
this.iter = iter;
}
*map<U>(func: (item: T) => U): IterableIterator<U> { 2
for (const value of this.iter) {
yield func(value);
}
}
*filter(pred: (item: T) => boolean): IterableIterator<T> { 2
for (const value of this.iter) {
if (pred(value)) {
yield value;
}
}
}
reduce(init: T, op: (x: T, y: T) => T): T { 2
let result: T = init;
for (const value of this.iter) {
result = op(result, value);
}
return result;
}
}
FluentIterable<T>我们可以从 中创建一个Iterable<T>,这样我们就可以将filter()/reduce()管道重写为更流畅的形式。我们创建一个Fluent-Iterable<T>,调用它,从它的结果filter()创建一个新的,然后调用它,如以下清单所示。 FluentIterable<T>reduce()
We can create a FluentIterable<T> out of an Iterable<T>, so we can rewrite our filter()/reduce() pipeline into a more fluent form. We create a Fluent-Iterable<T>, call filter() on it, create a new FluentIterable<T> from its result, and call reduce() on it, as the following listing shows.
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
常量结果:数字 =
新的 FluentIterable(
新的 FluentIterable(
inOrderIterator(root) 1
).filter((value) => value % 2 == 0) 2
).reduce(0, (x, y) => x + y); 3个
控制台日志(结果);let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
const result: number =
new FluentIterable(
new FluentIterable(
inOrderIterator(root) 1
).filter((value) => value % 2 == 0) 2
).reduce(0, (x, y) => x + y); 3
console.log(result);
Nowfilter()出现在 之前reduce(),很明显参数转到了那个函数。唯一的问题是我们需要在每次函数调用后创建一个新的Fluent-Iterable<T>。我们可以通过让我们的map()和filter()函数返回 aFluentIterable<T>而不是默认值来改进我们的 API IterableIterator<T>。请注意,我们不需要更改reduce(),因为reduce()返回类型的单个值T,而不是可迭代的。
Now filter() appears before reduce(), and it’s very clear that arguments go to that function. The only problem is that we need to create a new Fluent-Iterable<T> after each function call. We can improve our API by having our map() and filter() functions return a FluentIterable<T> instead of the default IterableIterator<T>. Note that we don’t need to change reduce(), because reduce() returns a single value of type T, not an iterable.
因为我们正在使用生成器,所以我们不能简单地更改返回类型。生成器的存在是为了为函数提供方便的语法,但它们总是返回一个IterableIterator<T>. 相反,我们可以将实现转移到几个私有方法——mapImpl()和——并处理公共方法和方法中从到的filterImpl()转换,如以下清单所示。 IterableIterator<T>FluentIterable<T>map()reduce()
Because we’re using generators, we can’t simply change the return type. Generators exist to provide convenient syntax for functions, but they always return an IterableIterator<T>. Instead, we can move the implementations to a couple of private methods—mapImpl() and filterImpl()—and handle the conversion from IterableIterator<T> to FluentIterable<T> in the public map() and reduce() methods, as shown in the following listing.
类 FluentIterable<T> {
iter: 可迭代<T>;
构造函数(iter: Iterable<T>) {
这个.iter = iter;
}
map<U>(func: (item: T) => U): FluentIterable<U> {
返回新的 FluentIterable(this.mapImpl(func)); 1个
}
private *mapImpl<U>(func: (item: T) => U): IterableIterator<U> {
for (this.iter 的常量值) { 2
收益函数(值);
}
}
filter<U>(pred: (item: T) => boolean): FluentIterable<T> {
返回新的 FluentIterable(this.filterImpl(pred)); 3个
}
private *filterImpl(pred: (item: T) => boolean): IterableIterator<T> {
for (this.iter 的常量值) { 4
如果(预测值(值)){
屈服值;
}
}
}
减少(初始化:T,操作:(x:T,y:T)=> T):T { 5
让结果:T = init;
for (this.iter 的常量值) {
结果=操作(结果,价值);
}
返回结果;
}
}class FluentIterable<T> {
iter: Iterable<T>;
constructor(iter: Iterable<T>) {
this.iter = iter;
}
map<U>(func: (item: T) => U): FluentIterable<U> {
return new FluentIterable(this.mapImpl(func)); 1
}
private *mapImpl<U>(func: (item: T) => U): IterableIterator<U> {
for (const value of this.iter) { 2
yield func(value);
}
}
filter<U>(pred: (item: T) => boolean): FluentIterable<T> {
return new FluentIterable(this.filterImpl(pred)); 3
}
private *filterImpl(pred: (item: T) => boolean): IterableIterator<T> {
for (const value of this.iter) { 4
if (pred(value)) {
yield value;
}
}
}
reduce(init: T, op: (x: T, y: T) => T): T { 5
let result: T = init;
for (const value of this.iter) {
result = op(result, value);
}
return result;
}
}
通过这个更新的实现,我们可以更轻松地链接算法,因为每个算法都返回一个FluentIterable包含所有算法的方法,如下一个清单所示。
With this updated implementation, we can more easily chain the algorithms, as each returns a FluentIterable that contains all the algorithms as methods, as shown in the next listing.
让 root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
常量结果:数字 =
new FluentIterable(inOrderIterator(root)) 1
.filter((value) => value % 2 == 0) 2
.reduce(0, (x, y) => x + y); 3个
控制台日志(结果);let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3);
root.right = new BinaryTreeNode(4);
const result: number =
new FluentIterable(inOrderIterator(root)) 1
.filter((value) => value % 2 == 0) 2
.reduce(0, (x, y) => x + y); 3
console.log(result);
现在,以真正流畅的方式,代码很容易从左到右阅读,我们可以使用非常自然的语法链接构成我们管道的任意数量的算法。大多数算法库都采用类似的方法,从而尽可能轻松地链接多个算法。
Now, in true fluent fashion, the code reads easily from left to right, and we can chain any number of algorithms that make up our pipeline with a very natural syntax. Most algorithm libraries take a similar approach, making it as easy as possible to chain multiple algorithms.
根据编程语言的不同,流畅的 API 方法的一个缺点是我们FluentIterable最终会包含所有算法,因此它是难以延伸。如果它是库的一部分,则调用代码在不修改类的情况下无法轻松添加新算法。C# 提供扩展方法,使我们能够在不修改其代码的情况下向类或接口添加方法。不过,并非所有语言都具有此类功能。也就是说,在大多数情况下,我们应该使用现有的算法库,而不是从头开始实现一个新的算法库。
Depending on the programming language, one downside of a fluent API approach is that our FluentIterable ends up containing all the algorithms, so it is difficult to extend. If it is part of a library, calling code can’t easily add a new algorithm without modifying the class. C# provides extension methods, which enable us to add methods to a class or interface without modifying its code. Not all languages have such features, though. That being said, in most situations, we should be using an existing algorithm library, not implementing a new one from scratch.
扩展FluentIterablewith take(),从迭代器返回前 n 个元素的算法。
Extend FluentIterable with take(), the algorithm that returns the first n elements from an iterator.
Extend FluentIterable with drop(), the algorithm that skips the first n elements of an iterator and returns the rest.
我们看到了通用数据结构如何赋予数据形状,而不管其特定类型参数T是什么。我们还研究了一组算法,这些算法使用迭代器来处理某种类型的值序列T,而不管该类型是什么。现在让我们看看下面清单中的一个场景,其中类型很重要:我们有一个renderAll()通用函数,它接受一个as 参数并在迭代器的每个元素上 Iterable<T>调用该方法。render()
We saw how a generic data structure gives shape to the data, regardless of what its specific type parameter T is. We also looked at a set of algorithms that uses iterators to process sequences of values of some type T, regardless of what that type is. Now let’s look at a scenario in the following listing in which the type matters: we have a renderAll() generic function that takes an Iterable<T> as argument and calls the render() method on each element of the iterator.
函数 renderAll<T>(iter: Iterable<T>): void { 1
for (iter 的常量项) {
item.render(); 2个
}
}.function renderAll<T>(iter: Iterable<T>): void { 1
for (const item of iter) {
item.render(); 2
}
}.
函数编译失败,错误信息如下:
The function fails to compile, with the following error message:
类型“T”上不存在属性“render”。
Property 'render' does not exist on type 'T'.
我们正在尝试调用render()泛型类型T,但我们不能保证该类型上存在这样的方法。对于这种类型的场景,我们需要一种方法来约束类型T,以便它只能被具有render()方法的类型实例化。
We are attempting to call render() on a generic type T, but we have no guarantee that such a method exists on the type. For this type of scenario, we need a way to constrain the type T so that it can be instantiated only with types that have a render() method.
约束告知编译器类型参数必须具备的能力。没有任何约束,类型参数可以是任何类型。一旦我们要求某些成员在泛型类型上可用,我们就会使用约束将允许的类型集限制为具有所需成员的类型。
Constraints inform the compiler about the capabilities that a type argument must have. Without any constraints, the type argument could be any type. As soon as we require certain members to be available on a generic type, we use constraints to restrict the set of allowed types to those that have the required members.
在我们的例子中,我们可以定义一个IRenderable声明方法的接口render(),如下一个清单所示。T然后我们可以通过使用extends关键字告诉编译器我们只接受类型参数来添加约束IRenderable。
In our case, we can define an IRenderable interface that declares a render() method, as shown in the next listing. Then we can add a constraint on T by using the extends keyword to tell the compiler that we accept only type arguments that are IRenderable.
接口 IRenderable { 1
渲染():无效;
}
函数 renderAll<T extends IRenderable >(iter: Iterable<T>): void { 2
for (iter 的常量项) {
item.render();
}
}interface IRenderable { 1
render(): void;
}
function renderAll<T extends IRenderable>(iter: Iterable<T>): void { 2
for (const item of iter) {
item.render();
}
}
大多数通用数据结构不需要限制它们的类型参数。我们可以将任何类型的值存储在链表、树或数组中。不过也有一些例外,例如哈希集。
Most generic data structures don’t need to constrain their type parameters. We can store values of any type in a linked list, a tree, or an array. There are a few exceptions, though, such as a hash set.
集合数据结构模拟数学集合,因此它存储唯一值,丢弃重复值。集合数据结构通常提供合并、相交和减去其他集合的方法。它们还提供了一种方法来检查给定值是否已经是集合的一部分。要检查一个值是否已经是集合的一部分,我们可以将它与集合中的每个元素进行比较,但这种方法并不是最有效的。与集合中的每个元素进行比较需要我们在最坏的情况下遍历整个集合。这样的遍历需要线性时间,或 O(n)。请参阅下一页的边栏“大 O 表示法”以进行复习。
A set data structure models a mathematical set, so it stores unique values, discarding duplicates. Set data structures usually provide methods to union, intersect, and subtract other sets. They also provide a way to check whether a given value is already part of the set. To check whether a value is already part of a set, we can compare it with every element of the set, but that approach is not the most efficient. Comparing with every element in the set requires us, in the worst case, to traverse the whole set. Such a traversal requires linear time, or O(n). See the sidebar “Big O notation” on the next page for a refresher.
更高效的实现可以散列每个值并将其存储在键值数据结构中,如散列映射或字典。这样的数据结构可以在常数时间或 O(1)中检索值,从而使它们更高效。散列集包装了散列映射,可以提供高效的成员资格检查。但它确实有一个约束:类型T需要提供一个哈希函数,它接受一个类型的值T并返回一个number:它的哈希值。
A more efficient implementation can hash each value and store it in a key-value data structure like a hash map or dictionary. Such data structures can retrieve a value in constant time, or O(1), making them more efficient. A hash set wraps a hash map and can provide efficient membership checks. But it does come with a constraint: the type T needs to provide a hash function, which takes a value of type T and returns a number: its hash value.
某些语言通过在其顶级类型上提供散列方法来确保所有值都可以散列。Java 顶级类型Object有一个hashCode()方法,而 C#Object顶级类型有一个GetHashCode()方法。但是如果一种语言没有,我们需要一个类型约束来确保只有可散列的类型可以存储在数据结构中。IHashable例如, 我们可以定义一个接口,并使其成为我们通用哈希映射或字典的键类型的类型约束。
Some languages ensure that all values can be hashed by providing a hash method on their top type. The Java top type, Object, has a hashCode() method, whereas the C# Object top type has a GetHashCode() method. But if a language doesn’t have that, we need a type constraint to ensure that only hash-able types can be stored in the data structures. We could define an IHashable interface, for example, and make it a type constraint on the key type of our generic hash map or dictionary.
大 O 表示法为函数执行所需的时间和空间提供了上限,因为它的参数趋向于特定值 n。我们不会深入探讨这个话题;相反,我们将概述一些常见的上限并解释它们的含义。
Big O notation provides an upper bound to the time and space required by a function to execute as its arguments tend toward a particular value n. We won’t go too deep into this topic; instead we’ll outline a few common upper bounds and explain what they mean.
Constant time或 O(1),意味着函数的执行时间不取决于它必须处理的项目数。first()例如,获取序列第一个元素的 函数对于 2 或 200 万个项目的序列运行速度一样快。
Constant time, or O(1), means that a function’s execution time does not depend on the number of items it has to process. The function first(), which takes the first element of a sequence, runs just as fast for a sequence of 2 or 2 million items, for example.
对数时间,或 O(log n),意味着该函数每一步将其输入减半,因此即使对于较大的 n 值,它也非常有效。一个例子是在排序序列中进行二分查找。
Logarithmic time, or O(log n), means that the function halves its input with each step, so it is very efficient even for large values of n. An example is binary search in a sorted sequence.
线性时间,或 O(n),意味着函数运行时间与其输入成比例增长。遍历一个序列是O(n),比如判断一个序列的所有元素是否满足某个谓词。
Linear time, or O(n), means that the function run time grows proportionally with its input. Looping over a sequence is O(n), such as determining whether all elements of a sequence satisfy some predicate.
二次时间或 O(n 2 ) 的效率远低于线性时间,因为运行时间的增长速度远快于输入大小的增长速度。序列上的两个嵌套循环的运行时间为 O(n 2 )。
Quadratic time, or O(n2), is much less efficient than linear, as the run time grows much faster than the size of the input. Two nested loops over a sequence have a run time of O(n2).
Linearithmic或 O(n log n) 不如线性有效,但比二次有效。最有效的比较排序算法是 O(n log n);我们无法用单个循环对序列进行排序,但我们可以比两个嵌套循环更快地完成排序。
Linearithmic, or O(n log n), is not as efficient as linear but more efficient than quadratic. The most efficient comparison sort algorithms are O(n log n); we can’t sort a sequence with a single loop, but we can do it faster than two nested loops.
正如时间复杂度设置了一个函数的运行时间如何随着其输入大小的增加而增加的上限一样,空间复杂度设置了一个函数随着其输入大小的增长而需要的额外内存量的上限。
Just as time complexity sets an upper bound on how the run time of a function increases with the size of its input, space complexity sets an upper bound on the amount of additional memory a function needs as the size of its input grows.
常量空间,或 O(1),意味着随着输入大小的增长,函数不需要更多空间。max()例如,我们的函数需要一些额外的内存来存储运行最大值和迭代器,但无论序列有多大,内存量都是恒定的 。
Constant space, or O(1), means that a function doesn’t need more space as the size of the input grows. Our max() function, for example, requires some extra memory to store the running maximum and the iterator, but the amount of memory is constant regardless of how large the sequence is.
线性空间,或 O(n),意味着函数需要的内存量与其输入的大小成正比。这种函数的一个例子是我们最初的in-Order()二叉树遍历,它将所有节点的值复制到一个数组中以提供树上的迭代器。
Linear space, or O(n), means that the amount of memory a function needs is proportional to the size of its input. An example of such a function is our original in-Order() binary tree traversal, which copied the values of all nodes into an array to provide an iterator over the tree.
算法往往比数据结构对其类型有更多的限制。如果我们想要对一组值进行排序,我们需要一种方法来比较这些值。同样,如果我们要确定一个序列的最小或最大元素,则该序列的元素需要具有可比性。
Algorithms tend to have more constraints on their types than data structures. If we want to sort a set of values, we need a way to compare those values. Similarly, if we want to determine the minimum or maximum element of a sequence, the elements of that sequence need to be comparable.
让我们看看max()下一个清单中通用算法的可能实现。首先,我们将声明一个IComparable<T>接口并约束我们的算法使用它。该接口声明了一个compareTo()方法。
Let’s look at a possible implementation of a max() generic algorithm in the next listing. First, we will declare an IComparable<T> interface and constrain our algorithm to use it. The interface declares a single compareTo() method.
枚举比较结果 { 1
少于,
平等的,
比...更棒
}
接口 IComparable<T> {
compareTo(value: T): 比较结果; 2
}enum ComparisonResult { 1
LessThan,
Equal,
GreaterThan
}
interface IComparable<T> {
compareTo(value: T): ComparisonResult; 2
}
现在让我们实现一个max()通用算法,该算法期望对一IComparable组值进行迭代并返回最大元素,如清单 10.16所示。我们需要处理迭代器没有值的情况,在这种情况下max()将返回undefined。因此,我们不会使用for...of循环;相反,我们将使用next().
Now let’s implement a max() generic algorithm that expects an iterator over an IComparable set of values and returns the maximum element, as shown in listing 10.16. We will need to handle the case in which the iterator has no values, in which case max() will return undefined. For that reason, we won’t use a for...of loop; rather, we will advance the iterator manually by using next().
函数 max<T extends IComparable<T>>(iter: Iterable<T>)
: 吨 | undefined { 1
let iterator: Iterator<T> = iter[Symbol.iterator](); 2个
让当前:IteratorResult<T> = iterator.next(); 3个
如果(当前完成)返回未定义; 4个
让结果:T = current.value; 5个
而(真){
current = iterator.next();
如果(当前完成)返回结果; 6个
如果(current.value.compareTo(结果)==
ComparisonResult.GreaterThan) {
结果 = 当前值; 7
}
}
}function max<T extends IComparable<T>>(iter: Iterable<T>)
: T | undefined { 1
let iterator: Iterator<T> = iter[Symbol.iterator](); 2
let current: IteratorResult<T> = iterator.next(); 3
if (current.done) return undefined; 4
let result: T = current.value; 5
while (true) {
current = iterator.next();
if (current.done) return result; 6
if (current.value.compareTo(result) ==
ComparisonResult.GreaterThan) {
result = current.value; 7
}
}
}
许多算法(例如max())需要它们所操作的类型中的某些东西。另一种方法是使比较成为函数本身的参数,而不是泛型类型约束。而不是IComparable<T>,max()可以期待第二个参数 - 一个compare()函数 - 从两个类型的参数T到 a ComparisonResult,如以下代码所示。
Many algorithms, such as max(), require certain things from the types they operate on. An alternative is to make the comparison an argument to the function itself as opposed to a generic type constraint. Instead of IComparable<T>, max() can expect a second argument—a compare() function—from two arguments of type T to a ComparisonResult, as shown in the following code.
函数 max<T>(iter: Iterable<T>,
compare: (x: T, y: T) => ComparisonResult ) 1
: 吨 | 不明确的 {
让迭代器: Iterator<T> = iter[Symbol.iterator]();
让当前:IteratorResult<T> = iterator.next();
如果(当前完成)返回未定义;
让结果:T = current.value;
而(真){
current = iterator.next();
如果(当前完成)返回结果;
if ( compare(current.value, result)
== ComparisonResult.GreaterThan) { 2
结果 = 当前值;
}
}
}function max<T>(iter: Iterable<T>,
compare: (x: T, y: T) => ComparisonResult) 1
: T | undefined {
let iterator: Iterator<T> = iter[Symbol.iterator]();
let current: IteratorResult<T> = iterator.next();
if (current.done) return undefined;
let result: T = current.value;
while (true) {
current = iterator.next();
if (current.done) return result;
if (compare(current.value, result)
== ComparisonResult.GreaterThan) { 2
result = current.value;
}
}
}
这种实现的好处是类型T不再受限,我们可以插入任何比较函数。缺点是对于具有自然顺序的类型(数字、温度、距离等),我们必须继续显式提供比较函数。好的算法库通常提供两种版本的算法:一种使用类型的自然比较,另一种调用者可以提供自己的算法。
The advantage of this implementation is that the type T is no longer constrained, and we can plug in any comparison function. The disadvantage is that for types that have a natural order (numbers, temperatures, distances, and so on), we have to keep supplying a compare function explicitly. Good algorithm libraries usually provide both versions of an algorithm: one that uses a type’s natural comparison and another for which callers can supply their own.
算法对T它所操作的类型所提供的方法和属性了解得越多,它就越能在其实现中利用这些方法和属性。接下来,让我们看看算法如何使用迭代器来提供更高效的实现。
The more an algorithm knows about the methods and properties that a type T it operates on provides, the more it can leverage those in its implementation. Next, let’s see how algorithms can use iterators to provide more efficient implementations.
clamp()实现一个接受值、低值和高值的通用函数。如果该值在低-高范围内,则返回值。如果该值小于低,则返回低。如果值大于 high,则返回 high。使用IComparable本节中定义的接口。
Implement a generic function clamp() that takes a value, a low, and a high. If the value is within the low-high range, it returns the values. If the value is less than low, it returns low. If the value is larger than high, it returns high. Use the IComparable interface defined in this section.
到目前为止,我们已经了解了以线性方式处理序列的算法。map(), filter(), reduce(), 和max()all 从头到尾遍历一系列值。它们都在线性时间(与序列的大小成正比)和恒定空间内运行。(无论序列的大小如何,内存需求都是恒定的。)让我们看看另一种算法:reverse()。
So far, we’ve looked at algorithms that process a sequence in a linear fashion. map(), filter(), reduce(), and max() all iterate over a sequence of values from start to finish. They all run in linear time (proportionate to the size of the sequence) and constant space. (Memory requirements are constant regardless of the size of the sequence.) Let’s look at another algorithm: reverse().
该算法采用一个序列并将其反转,使最后一个元素成为第一个元素,倒数第二个元素成为第二个元素,依此类推。一种实现方式是将其输入的所有元素压入堆栈,然后将它们弹出,如图10.1和代码清单 10.18reverse()所示。
This algorithm takes a sequence and reverses it, making the last element the first one, the second-to-last element the second one, and so on. One way to implement reverse() is to push all elements of its input into a stack and then pop them out, as shown in figure 10.1 and listing 10.18.
function *reverse<T>(iter: Iterable<T>): IterableIterator<T> { 1
let stack: T[] = []; 2个
for (iter 的常量值) {
堆栈.push(值); 3个
}
而(真){
让值:T | 未定义 = stack.pop(); 4个
如果(值==未定义)返回; 5个
屈服值; 6个
}
}function *reverse<T>(iter: Iterable<T>): IterableIterator<T> { 1
let stack: T[] = []; 2
for (const value of iter) {
stack.push(value); 3
}
while (true) {
let value: T | undefined = stack.pop(); 4
if (value == undefined) return; 5
yield value; 6
}
}
这个实现很简单,但不是最有效的。虽然它在线性时间运行,但它也需要线性空间。输入序列越大,该算法将其所有元素压入堆栈所需的内存就越多。
This implementation is straightforward but not the most efficient. Although it runs in linear time, it also requires linear space. The larger the input sequence, the more memory this algorithm will need to push all its elements onto the stack.
让我们暂时把迭代器放在一边,看看我们如何对数组实现高效的反转,如清单 10.19所示。我们可以就地执行此操作,从两端开始交换数组的元素,而不需要额外的堆栈(图 10.2)。
Let’s set iterators aside for now and look at how we would implement an efficient reverse over an array, as shown in listing 10.19. We can do this in-place, swapping the elements of the array starting from both ends without requiring an additional stack (figure 10.2).
function reverse<T>(values: T[]): void { 1
let begin: number = 0; 2
让结束:number = values.length; 2个
while (begin < end) { 3
const temp: T = values[begin]; 4
值[开始] = 值[结束 - 1]; 4 个
值 [end - 1] = temp; 4个
开始++; 5
结束--; 5个
}
}function reverse<T>(values: T[]): void { 1
let begin: number = 0; 2
let end: number = values.length; 2
while (begin < end) { 3
const temp: T = values[begin]; 4
values[begin] = values[end - 1]; 4
values[end - 1] = temp; 4
begin++; 5
end--; 5
}
}
如我们所见,此实现比前一个更有效。它仍然是线性时间,因为我们需要触及序列的每个元素(不可能在不接触每个元素的情况下反转序列),但它需要恒定的空间来运行。temp与需要与其输入一样大的堆栈的先前版本不同,这个版本使用type 的临时值T,无论输入有多大。
As we can see, this implementation is more efficient than the preceding one. It is still linear time, as we need to touch every element of the sequence (it is impossible to reverse a sequence without touching each element), but it requires constant space to run. Unlike the previous version, which needed a stack as large as its input, this one uses the temporary temp of type T, regardless of how large the input is.
我们可以概括这个例子并为任何数据结构提供有效的反向算法吗?我们可以,但我们需要调整迭代器的概念。Iterator<T>,Iterable<T>以及两者的组合 ,IterableIterator<T>是 TypeScript 在 JavaScript ES6 标准之上提供的接口。现在我们将超越这一点,看看一些不属于语言标准的迭代器。
Can we generalize this example and provide an efficient reverse algorithm for any data structure? We can, but we need to tweak our notion of iterators. Iterator<T>, Iterable<T>, and the combination of the two, IterableIterator<T>, are interfaces that TypeScript provides over the JavaScript ES6 standard. Now we’ll go beyond that and look at some iterators that are not part of the language standard.
JavaScript 迭代器允许我们检索值并前进直到序列耗尽。如果我们想运行就地算法,我们需要更多的能力。我们不仅需要能够读取给定位置的值,还需要能够设置它们。在我们的reverse()例子中,我们从序列的两端开始,在中间结束,这意味着迭代器无法判断它何时完全由自己完成。我们知道当和相互传递reverse()时就完成了,所以我们需要一种方法来判断两个迭代器何时相同。 beginend
JavaScript iterators allow us to retrieve values and advance until the sequence is exhausted. If we want to run an in-place algorithm, we need a few more capabilities. We also need to be able not only to read values at a given position, but also set them. In our reverse() case, we start from both ends of the sequence and end in the middle, which means that an iterator can’t tell when it is done all by itself. We know that reverse() is done when begin and end pass each other, so we need a way to tell when two iterators are the same.
为了支持高效的算法,让我们将迭代器重新定义为一组接口,每个接口都描述了额外的功能。首先,让我们定义一个IReadable<T>公开get()返回 type 值的方法的方法T。我们将使用此方法从迭代器中读取值。我们还将定义一个IIncrementable<T>公开increment()可用于推进迭代器的方法的方法,如以下清单所示。
To support efficient algorithms, let’s redefine our iterators as a set of interfaces, each describing additional capabilities. First, let’s define an IReadable<T> that exposes a get() method returning a value of type T. We will use this method to read a value from an iterator. We’ll also define an IIncrementable<T> that exposes an increment() method we can use to advance our iterator, as the following listing shows.
接口 IReadable<T> {
得到():T; 1个
}
接口 IIncrementable<T> {
增量():无效; 2
}interface IReadable<T> {
get(): T; 1
}
interface IIncrementable<T> {
increment(): void; 2
}
这两个接口几乎足以支持我们原有的线性遍历算法如map(). 最后缺少的是弄清楚什么时候应该停止。我们知道迭代器无法自行判断何时完成,因为有时它不需要遍历整个序列。我们将引入相等的概念:一个迭代器begin和一个迭代器end在指向同一个元素时是相等的。这是多比标准Iterator- <T>实现更灵活。我们可以初始化end为序列最后一个元素之后的一个元素。然后我们可以前进begin直到它等于end,在这种情况下我们将知道我们已经遍历了整个序列。但我们也可以end向后移动,直到它指向序列的第一个元素——这是我们无法用标准Iterator<T>. (图 10.3)。
These two interfaces are almost enough to support our original linear traversal algorithms such as map(). The last thing missing is figuring out when we should stop. We know that an iterator can’t tell by itself when it is done, as sometimes it doesn’t need to traverse the whole sequence. We’ll introduce the concept of equality: an iterator begin and an iterator end are equal when they point to the same element. This is much more flexible than the standard Iterator- <T> implementation. We can initialize end to be one element after the last element of a sequence. Then we can advance begin until it is equal to end, in which case we’ll know that we’ve traversed the whole sequence. But we can also move end back until it points to the first element of the sequence—something we couldn’t have done with the standard Iterator<T>. (figure 10.3).
让我们IInputIterator<T>在下一个清单中定义一个接口,它实现了IReadable<T>和IIncrementable<T>,加上一个equals()我们可以用来比较两个迭代器的方法。
Let’s define an IInputIterator<T> interface in the next listing as an interface that implements both IReadable<T> and IIncrementable<T>, plus an equals() method we can use to compare two iterators.
接口 IInputIterator<T> 扩展 IReadable<T>, IIncrementable<T> {
等于(其他:IInputIterator<T>):布尔值;
}interface IInputIterator<T> extends IReadable<T>, IIncrementable<T> {
equals(other: IInputIterator<T>): boolean;
}
迭代器本身无法再确定它何时遍历了整个序列。一个序列现在由两个迭代器定义——一个指向序列开头的迭代器和一个指向序列最后一个元素之后的迭代器。
The iterator itself can no longer determine when it has traversed the whole sequence. A sequence is now defined by two iterators—an iterator pointing to the start of the sequence and an iterator pointing to one past the last element of the sequence.
有了这些可用的接口,让我们在下一个清单中更新第 9 章中的链表迭代器。我们的链表被实现为LinkedListNode<T>具有一个value属性的类型,一个next属性可以是列表中的最后一个LinkedListNode<T>节点undefined。
With these interfaces available, let’s update our linked list iterator from chapter 9 in the next listing. Our linked list is implemented as the type LinkedListNode<T> with a value property and a next property that can be a LinkedListNode<T> or undefined for the last node in the list.
类 LinkedListNode<T> {
值:T;
下一个:LinkedListNode<T> | 不明确的;
构造函数(值:T){
this.value = 值;
}
}class LinkedListNode<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
}
让我们看看我们如何在下面的清单中为这个链表的一对迭代器建模。首先,我们需要实现一个LinkedListInputIterator<T>满足IInputIterator<T>链表新接口的 。
Let’s see how we can model a pair of iterators over this linked list in the following listing. First, we’ll need to implement a LinkedListInputIterator<T> that satisfies our new IInputIterator<T> interface for a linked list.
类 LinkedListInputIterator<T> 实现 IInputIterator<T> {
私有节点:LinkedListNode<T> | 不明确的;
构造函数(节点:LinkedListNode<T> | 未定义){
this.node = 节点;
}
增量():无效{ 1
如果(!this.node)抛出错误();
this.node = this.node.next;
}
得到():T { 2
如果(!this.node)抛出错误();
返回这个节点值;
}
等于(其他:IInputIterator<T>):布尔值 { 3
return this.node == (<LinkedListInputIterator<T>>other).node;
}
}class LinkedListInputIterator<T> implements IInputIterator<T> {
private node: LinkedListNode<T> | undefined;
constructor(node: LinkedListNode<T> | undefined) {
this.node = node;
}
increment(): void { 1
if (!this.node) throw Error();
this.node = this.node.next;
}
get(): T { 2
if (!this.node) throw Error();
return this.node.value;
}
equals(other: IInputIterator<T>): boolean { 3
return this.node == (<LinkedListInputIterator<T>>other).node;
}
}
begin现在我们可以通过初始化为列表的头部和endbe 来在链表上创建一对迭代器undefined,如以下代码所示。
Now we can create a pair of iterators over a linked list by initializing begin to be the head of the list and end to be undefined, as shown in the following code.
const head: LinkedListNode<number> = new LinkedListNode(0); 1个 head.next = new LinkedListNode(1); head.next.next = new LinkedListNode(2); 让我们开始吧: IInputIterator<number> = new LinkedListInputIterator(head); 2 让结束:IInputIterator<number> = new LinkedListInputIterator(undefined); 3个
const head: LinkedListNode<number> = new LinkedListNode(0); 1 head.next = new LinkedListNode(1); head.next.next = new LinkedListNode(2); let begin: IInputIterator<number> = new LinkedListInputIterator(head); 2 let end: IInputIterator<number> = new LinkedListInputIterator(undefined); 3
我们称其为输入迭代器,因为我们可以使用该方法从中读取值get()。
We call this an input iterator because we can read values from it by using the get() method.
输入迭代器是可以遍历序列一次并提供其值的迭代器。它无法再次重播这些值,因为这些值可能不再可用。输入迭代器不必遍历持久数据结构;它还可以提供来自生成器或其他来源的值(图 10.4)。
An input iterator is an iterator that can traverse a sequence once and provide its values. It can’t replay the values a second time, as the values may no longer be available. An input iterator doesn’t have to traverse a persistent data structure; it can also provide values from a generator or some other source (figure 10.4).
让我们也定义一个输出迭代器作为我们可以写入的迭代器。为此,我们将声明一个IWritable<T>带有方法的接口set(),并将、和方法IOutput-Iterator<T>组合起来,如下一个清单所示。 IWritable<T>IIncrementable<T>equals()
Let’s also define an output iterator as an iterator we can write to. For that, we’ll declare an IWritable<T> interface with a set() method and have our IOutput-Iterator<T> be the combination of IWritable<T>, IIncrementable<T>, and an equals() method, as shown in the next listing.
接口 IWritable<T> {
设置(值:T):无效;
}
接口 IOutputIterator<T> 扩展 IWritable<T>, IIncrementable<T> {
等于(其他:IOutputIterator<T>):布尔值;
}interface IWritable<T> {
set(value: T): void;
}
interface IOutputIterator<T> extends IWritable<T>, IIncrementable<T> {
equals(other: IOutputIterator<T>): boolean;
}
我们可以将值写入这种类型的迭代器,但我们不能读回它们。
We can write values to this type of iterator, but we can’t read them back.
输出迭代器是可以遍历序列并向其写入值的迭代器;它不必能够读回它们。输出迭代器不必遍历持久数据结构;它还可以将值写入其他输出。
An output iterator is an iterator that can traverse a sequence and write values to it; it doesn’t have to be able to read them back. An output iterator doesn’t have to traverse a persistent data structure; it can also write values to other outputs.
让我们实现一个写入控制台的输出迭代器。写入输出流是输出迭代器最常见的用例:那是我们可以输出数据但无法读回的时候。我们可以将数据(但无法读取)写入网络连接、标准输出、标准错误等。在我们的例子中,推进迭代器不做任何事情,而设置一个值 calls console.log(),如下一个清单所示。
Let’s implement an output iterator that writes to the console. Writing to an output stream is the most common use case for an output iterator: that’s when we can output data but can’t read it back. We can write data (without being able to read it) to a network connection, standard output, standard error, and so on. In our case, advancing the iterator doesn’t do anything, whereas setting a value calls console.log(), as shown in the next listing.
类 ConsoleOutputIterator<T> 实现 IOutputIterator<T> {
设置(值:T):无效{
控制台日志(值); 1个
}
增量():无效{} 2
等于(其他:IOutputIterator<T>):布尔值{
返回假; 3个
}
}class ConsoleOutputIterator<T> implements IOutputIterator<T> {
set(value: T): void {
console.log(value); 1
}
increment(): void { } 2
equals(other: IOutputIterator<T>): boolean {
return false; 3
}
}
现在我们有一个接口,它描述了一个输入迭代器和一个在我们的链表上实现的具体实例。我们还有一个描述输出迭代器的接口和一个记录到控制台的具体实现。有了这些部分,我们可以提供清单 10.27map()中的替代实现。
Now we have an interface that describes an input iterator and a concrete instance of an implementation over our linked list. We also have an interface that describes an output iterator and a concrete implementation that logs to the console. With these pieces in place, we can provide an alternative implementation of map() in listing 10.27.
这个新版本的map()将接受一对定义序列的输入迭代器和一个输出迭代器作为参数begin,end它将out在其中写入将给定函数映射到序列上的结果。因为我们不再使用标准的 JavaScript,所以我们失去了一些语法糖——没有yield也没有for...of循环。
This new version of map() will take as argument a pair of begin and end input iterators that define a sequence and an output iterator out, where it will write the results of mapping the given function over the sequence. Because we are no longer using standard JavaScript, we lose some of the syntactic sugar—no yield and no for...of loops.
函数映射<T, U>(
开始:IInputIterator<T>,结束:IInputIterator<T>, 1
输出:IOutputIterator<U>, 2
函数:(值:T)=> U):void {
while (!begin.equals(end)) { 3
out.set(func(begin.get())); 4个
begin.increment(); 5
输出增量(); 5个
}
}function map<T, U>(
begin: IInputIterator<T>, end: IInputIterator<T>, 1
out: IOutputIterator<U>, 2
func: (value: T) => U): void {
while (!begin.equals(end)) { 3
out.set(func(begin.get())); 4
begin.increment(); 5
out.increment(); 5
}
}
这个版本和map()基于native的一样通用Iterable-Iterator<T>:我们可以提供any IInputIterator<T>,遍历链表的,按顺序遍历树的,等等。我们还可以提供任何一种IOutput-Iterator<T>写入控制台或写入数组的方式。
This version of map() is as general as the one based on the native Iterable-Iterator<T>: we can provide any IInputIterator<T>, one that traverses a linked list, one that traverses a tree in order, and so on. We can also provide any IOutput-Iterator<T>—one that writes to the console or one that writes to an array.
到目前为止,这并没有给我们带来太多好处。我们有一个替代实现,它不能利用 TypeScript 提供的特殊语法。但这些迭代器只是基本的构建块。我们可以定义更强大的迭代器,接下来我们将看看这些。
So far, this doesn’t gain us much. We have an alternative implementation that can’t leverage the special syntax that TypeScript provides. But these iterators are just the basic building blocks. We can define more-powerful iterators, and we’ll look at these next.
让我们采用另一种常见的算法:find()。该算法采用一系列值和一个谓词,并返回谓词返回的第一个元素 true。我们可以使用标准来实现这一点Iterable<T>,如以下清单所示。
Let’s take another common algorithm: find(). This algorithm takes a sequence of values and a predicate, and returns the first element for which the predicate returns true. We can implement this by using the standard Iterable<T>, as the following listing shows.
函数 find<T>(iter: Iterable<T>,
pred: (值: T) => 布尔值: T | 不明确的 {
for (iter 的常量值) {
如果(预测值(值)){
返回值;
}
}
返回未定义;
}function find<T>(iter: Iterable<T>,
pred: (value: T) => boolean): T | undefined {
for (const value of iter) {
if (pred(value)) {
return value;
}
}
return undefined;
}
这有效,但不是那么有用。如果我们想在找到它之后更改该值怎么办?如果我们在数字链表中搜索第一次出现的 42 以便我们可以用 0 替换它,返回 42 对我们没有帮助。结果也可能是find()a boolean,因为这个函数只告诉我们是否该值存在于序列中。
This works, but it’s not that useful. What if we want to change the value after we find it? If we are searching over a linked list of numbers for the first occurrence of 42 so that we can replace it with 0, it doesn’t help us that find() returns 42. The result may as well be a boolean, as this function tells us only whether the value exists in the sequence.
如果我们不返回值本身,而是得到一个指向该值的迭代器呢?开箱即用的 JavaScriptIterator<T>是只读的。我们已经看到了如何创建一个迭代器,我们也可以通过它来设置值。对于这种情况,我们需要可读和可写迭代器的组合。让我们定义一个前向迭代器。
What if, instead of returning the value itself, we get an iterator pointing to that value? The out-of-the-box JavaScript Iterator<T> is read-only. We’ve seen how to create an iterator through which we can also set values. For this scenario, we’ll need a combination of readable and writable iterators. Let’s define a forward iterator.
前向迭代器是可以前进的迭代器,可以读取其当前位置的值,并更新该值。前向迭代器也可以被克隆,因此推进迭代器的一个副本不会推进克隆。这很重要,因为它允许我们多次遍历一个序列,这与输入和输出迭代器不同(图 10.5)。
A forward iterator is an iterator that can be advanced, can read the value at its current position, and update that value. A forward iterator can also be cloned, so that advancing one copy of the iterator does not advance the clone. This is important, as it allows us to traverse a sequence multiple times, unlike input and output iterators (figure 10.5).
下一个清单中显示的界面是、、、 和方法IForwardIterator<T>的组合。 IReadable<T>IWritable<T>IIncrementable<T>equals()clone()
Our IForwardIterator<T> interface shown in the next listing is a combination of IReadable<T>, IWritable<T>, IIncrementable<T>, and the equals() and clone() methods.
接口 IForwardIterator<T> 扩展
IReadable<T>, IWritable<T>, IIncrementable<T> {
等于(其他: IForwardIterator<T>):布尔值;
克隆(): IForwardIterator<T>;
}interface IForwardIterator<T> extends
IReadable<T>, IWritable<T>, IIncrementable<T> {
equals(other: IForwardIterator<T>): boolean;
clone(): IForwardIterator<T>;
}
例如,让我们实现接口以迭代以下清单中的链表。我们将更新我们的LinkedListIterator<T>以提供新界面所需的其他方法。
As an example, let’s implement the interface to iterate over our linked list in the following listing. We’ll update our LinkedListIterator<T> to also provide the additional methods required by our new interface.
类 LinkedListIterator<T> 实现IForwardIterator<T> { 1
私有节点:LinkedListNode<T> | 不明确的;
构造函数(节点:LinkedListNode<T> | 未定义){
this.node = 节点;
}
增量():无效{
如果(!this.node)返回;
this.node = this.node.next;
}
得到():T {
如果(!this.node)抛出错误();
返回这个节点值;
}
set(value: T): void { 2
if (!this.node) throw Error();
this.node.value = 值;
}
equals(other: IForwardIterator<T> ): boolean { 3
返回 this.node == (<LinkedListIterator<T>>other).node;
}
clone(): IForwardIterator<T> { 4
return new LinkedListIterator(this.node);
}
}class LinkedListIterator<T> implements IForwardIterator<T> { 1
private node: LinkedListNode<T> | undefined;
constructor(node: LinkedListNode<T> | undefined) {
this.node = node;
}
increment(): void {
if (!this.node) return;
this.node = this.node.next;
}
get(): T {
if (!this.node) throw Error();
return this.node.value;
}
set(value: T): void { 2
if (!this.node) throw Error();
this.node.value = value;
}
equals(other: IForwardIterator<T>): boolean { 3
return this.node == (<LinkedListIterator<T>>other).node;
}
clone(): IForwardIterator<T> { 4
return new LinkedListIterator(this.node);
}
}
现在让我们看一下find()它的一个版本,它接受一对beginandend迭代器,并返回一个指向第一个满足谓词的元素的迭代器,如下一个清单所示。有了这个版本,我们可以在找到它时更新它。
Now let’s look at a version of find() that takes a pair of begin and end iterators, and returns an iterator pointing to the first element satisfying the predicate, shown in the next listing. With this version, we can update the value when we find it.
函数查找<T>(
开始: IForwardIterator<T>, 结束: IForwardIterator<T>, 1
pred: (value: T) => boolean): IForwardIterator<T> { 2
while (!begin.equals(end)) { 3
如果(预测(开始。得到())){
回归开始; 4个
}
begin.increment(); 5个
}
返回端; 6
}function find<T>(
begin: IForwardIterator<T>, end: IForwardIterator<T>, 1
pred: (value: T) => boolean): IForwardIterator<T> { 2
while (!begin.equals(end)) { 3
if (pred(begin.get())) {
return begin; 4
}
begin.increment(); 5
}
return end; 6
}
让我们使用数字链表,我们刚刚实现的迭代器遍历链表,并应用此算法找到第一个等于的值42并将其替换为 a 0,如以下代码所示。
Let’s use a linked list of numbers, the iterator we just implemented to traverse a linked list, and apply this algorithm to find the first value equal to 42 and replace it with a 0, as shown in the following code.
让 head: LinkedListNode<number> = new LinkedListNode(1); 1个
head.next = new LinkedListNode(2);
head.next.next = new LinkedListNode(42);
让我们开始吧: IForwardIterator<number> =
新的链表迭代器(头); 2个
让结束: IForwardIterator<number> =
新的链表迭代器(未定义); 2个
让 iter: IForwardIterator<number> =
查找(开始,结束,(值:数字)=> 值 == 42); 3个
如果 (!iter.equals(end)) { 4
iter.set(0); 5
}let head: LinkedListNode<number> = new LinkedListNode(1); 1
head.next = new LinkedListNode(2);
head.next.next = new LinkedListNode(42);
let begin: IForwardIterator<number> =
new LinkedListIterator(head); 2
let end: IForwardIterator<number> =
new LinkedListIterator(undefined); 2
let iter: IForwardIterator<number> =
find(begin, end, (value: number) => value == 42); 3
if (!iter.equals(end)) { 4
iter.set(0); 5
}
前向迭代器非常强大,因为它们可以遍历序列任意次数并修改它。这个特性允许我们实现不需要复制整个数据序列来转换它的就地算法。最后,让我们来解决一下我们在本节开始时使用的算法:reverse()。
Forward iterators are extremely powerful, as they can traverse a sequence any number of times and also modify it. This feature allows us to implement in-place algorithms that don’t need to copy over a whole sequence of data to transform it. Finally, let’s tackle the algorithm with which we started this section: reverse().
正如我们在数组实现中看到的,就地reverse()从数组的两端开始并交换元素,增加前面的索引并减少后面的索引,直到两者交叉。
As we saw in the array implementation, an in-place reverse() starts from both ends of the array and swaps elements, incrementing the front index and decrementing the back index until the two cross.
我们可以将数组实现概括为适用于任何序列,但是我们的迭代器需要一个额外的功能:递减其位置的能力。具有这种能力的迭代器称为双向迭代器。
We can generalize the array implementation to work with any sequence, but we need one extra capability on our iterator: the ability to decrement its position. An iterator with this ability is called a bidirectional iterator.
双向迭代器具有与正向迭代器相同的功能;此外,它可以递减。换句话说,双向迭代器可以向前和向后遍历一个序列(图 10.6)。
A bidirectional iterator has the same capabilities as a forward iterator; additionally, it can be decremented. In other words, a bidirectional iterator can traverse a sequence both forward and backward (figure 10.6).
让我们定义一个IBidirectionalIterator<T>类似于IForward-Iterator<T>带有附加decrement()方法的接口的接口。注意并不是所有的数据结构都能支持这样的迭代器,比如我们的链表。因为一个节点只引用它的后继节点,所以我们不能向后移动到前一个节点。但是我们可以在双向链表上提供一个双向迭代器,其中一个节点持有对其后继者和前任者或数组的引用。让我们在下一个清单中 实现一个ArrayIterator<T>as 。IBidirectionalIterator<T>
Let’s define an IBidirectionalIterator<T> interface similar to IForward-Iterator<T> interface with an additional decrement() method. Note that not all data structures can support such an iterator, such as our linked list. Because a node has a reference only to its successor, we cannot move backward to the preceding node. But we can provide a bidirectional iterator over a doubly linked list, in which a node holds references to both its successor and its predecessor or an array. Let’s implement an ArrayIterator<T> as an IBidirectionalIterator<T> in the next listing.
接口 IBidirectionalIterator<T> 扩展
IReadable<T>, IWritable<T>, IIncrementable<T> {
递减():无效; 1个
等于(其他:IBidirectionalIterator<T>):布尔值;
克隆():IBidirectionalIterator<T>;
}
类 ArrayIterator<T> 实现 IBidirectionalIterator<T> {
私有数组:T[];
私有索引:数字;
构造函数(数组:T[],索引:数字){
this.array = 数组;
this.index = 索引;
}
得到():T {
返回 this.array[this.index];
}
设置(值:T):无效{
this.array[this.index] = 值;
}
增量():无效{
这个。索引++;
}
递减():无效{
这个.index--;
}
等于(其他:IBidirectionalIterator<T>):布尔值{
返回 this.index == (<ArrayIterator<T>>other).index;
}
克隆():IBidirectionalIterator<T>{
返回新的 ArrayIterator(this.array, this.index);
}
}interface IBidirectionalIterator<T> extends
IReadable<T>, IWritable<T>, IIncrementable<T> {
decrement(): void; 1
equals(other: IBidirectionalIterator<T>): boolean;
clone(): IBidirectionalIterator<T>;
}
class ArrayIterator<T> implements IBidirectionalIterator<T> {
private array: T[];
private index: number;
constructor(array: T[], index: number) {
this.array = array;
this.index = index;
}
get(): T {
return this.array[this.index];
}
set(value: T): void {
this.array[this.index] = value;
}
increment(): void {
this.index++;
}
decrement(): void {
this.index--;
}
equals(other: IBidirectionalIterator<T>): boolean {
return this.index == (<ArrayIterator<T>>other).index;
}
clone(): IBidirectionalIterator<T> {
return new ArrayIterator(this.array, this.index);
}
}
现在让我们reverse()根据一对begin和end双向迭代器来实现。当两个迭代器相遇时,我们将交换值、增量begin、减量和停止。end我们必须确保两个迭代器永远不会相互传递,所以一旦我们移动其中一个,我们就会检查它们是否相遇。
Now let’s implement reverse() in terms of a pair of begin and end bidirectional iterators. We will swap the values, increment begin, decrement end, and stop when the two iterators meet. We must make sure that the two iterators never pass each other, so as soon as we move one of them, we check whether they met.
函数反转<T>(
开始:IBidirectionalIterator<T>,结束:IBidirectionalIterator<T>
): 空白 {
while (!begin.equals(end)) { 1
end.decrement(); 2
if (begin.equals(end)) 返回; 3个
const temp: T = begin.get(); 4
begin.set(end.get()); 4
end.set(temp); 4个
begin.increment(); 5个
}
}function reverse<T>(
begin: IBidirectionalIterator<T>, end: IBidirectionalIterator<T>
): void {
while (!begin.equals(end)) { 1
end.decrement(); 2
if (begin.equals(end)) return; 3
const temp: T = begin.get(); 4
begin.set(end.get()); 4
end.set(temp); 4
begin.increment(); 5
}
}
让我们在以下清单中的一组数字上尝试一下。
Let’s try it out on an array of numbers in the following listing.
让数组:number[] = [1, 2, 3, 4, 5];
让我们开始:IBidirectionalIterator<number>
= 新的数组迭代器(数组,0); 1个
让结束:IBidirectionalIterator<number>
= new ArrayIterator(array, array.length); 2个
反向(开始,结束);
控制台日志(数组); 3个let array: number[] = [1, 2, 3, 4, 5];
let begin: IBidirectionalIterator<number>
= new ArrayIterator(array, 0); 1
let end: IBidirectionalIterator<number>
= new ArrayIterator(array, array.length); 2
reverse(begin, end);
console.log(array); 3
使用双向迭代器,我们可以概括出一种高效的就地reverse()处理任何我们可以在两个方向上遍历的数据结构。我们扩展了原始算法,该算法仅限于数组以处理任何IBidirectional-Iterator<T>. 我们可以应用相同的算法来反转双向链表和我们可以前后移动迭代器的任何其他数据结构。
Using bidirectional iterators, we can generalize an efficient, in-place reverse() to work on any data structure that we can traverse in two directions. We extended the original algorithm, which was limited to arrays to work with any IBidirectional-Iterator<T>. We can apply the same algorithm to reverse a doubly linked list and any other data structure over which we can move an iterator backward and forward.
请注意,我们当然也可以反转单链表,但这样的算法不能泛化。当我们反转一个单链表时,我们改变了结构,因为我们将对每个下一个元素的引用翻转为引用前一个元素。这样的算法与其操作的数据结构紧密耦合,无法推广。相比之下,我们reverse()需要双向迭代器的泛型对于任何可以提供此类迭代器的数据结构都以相同的方式工作。
Note that we can also reverse a singly linked list, of course, but such an algorithm does not generalize. When we reverse a singly linked list, we alter the structure, as we’re flipping references to each next element to refer to the previous element instead. Such an algorithm is tightly coupled to the data structure it operates on and can’t be generalized. By contrast, our generic reverse() that requires a bidirectional iterator works the same way for any data structure that can provide such an iterator.
有些算法对它们的迭代器的要求比对increment()和 的要求更多decrement()。一个很好的例子是排序算法。一个高效的 O(n log n) 排序,如快速排序,将不得不绕过它正在排序的数据结构,访问任意位置的元素。为此,双向迭代器是不够的。我们需要一个随机访问迭代器。
There are algorithms that require more from their iterators than increment() and decrement(). A good example is sorting algorithms. An efficient, O(n log n) sort such as quicksort will have to jump around the data structure that it is sorting, accessing elements at arbitrary locations. For this purpose, a bidirectional iterator is not enough. We need a random-access iterator.
随机访问迭代器可以在常数时间内向前和向后跳转任意给定数量的元素。与可以一次递增或递减一步的双向迭代器不同,随机访问迭代器可以移动任意数量的元素(图 10.7)。
A random-access iterator can jump forward and backward any given number of elements in constant time. Unlike a bidirectional iterator, which can be incremented or decremented one step at a time, a random-access iterator can move any number of elements (figure 10.7).
数组是随机可访问数据结构的一个很好的例子,我们可以在其中索引并快速检索任何元素。相比之下,对于双向链表,我们需要遍历后继或前导引用才能到达元素。双向链表不支持随机访问迭代器。
Arrays are good examples of random-accessible data structures, in which we can index and quickly retrieve any element. By contrast, with a doubly linked list, we need to traverse through successor or predecessor references to reach an element. A doubly linked list cannot support a random-access iterator.
让我们将 an 定义IRandomAccessIterator<T>为一个迭代器,它不仅支持 的所有功能IBidirectionalIterator<T>,而且还move()支持移动迭代器 n 个元素的方法。对于随机访问迭代器,判断两个迭代器之间的距离也很有用。我们将distance()在下面的清单中添加一个返回两个迭代器之间差异的方法。
Let’s define an IRandomAccessIterator<T> as an iterator that supports not only all the capabilities of IBidirectionalIterator<T>, but also a move() method that moves the iterator n elements. With random access iterators, it’s also useful to tell how far apart two iterators are. We will add a distance() method that returns the difference between two iterators in the following listing.
接口 IRandomAccessIterator<T>
扩展 IReadable<T>, IWritable<T>, IIncrementable<T> {
递减():无效;
等于(其他:IRandomAccessIterator<T>):布尔值;
克隆(): IRandomAccessIterator<T> ;
移动(n:数字):无效;
距离(其他:IRandomAccessIterator<T>):数字;
}interface IRandomAccessIterator<T>
extends IReadable<T>, IWritable<T>, IIncrementable<T> {
decrement(): void;
equals(other: IRandomAccessIterator<T>): boolean;
clone(): IRandomAccessIterator<T>;
move(n: number): void;
distance(other: IRandomAccessIterator<T>): number;
}
让我们ArrayIterator<T>在下一个清单中更新我们的实现IRandom-AccessIterator<T>。
Let’s update our ArrayIterator<T> in the next listing to implement IRandom-AccessIterator<T>.
类 ArrayIterator<T> 实现IRandomAccessIterator<T> {
私有数组:T[];
私有索引:数字;
构造函数(数组:T[],索引:数字){
this.array = 数组;
this.index = 索引;
}
得到():T {
返回 this.array[this.index];
}
设置(值:T):无效{
this.array[this.index] = 值;
}
增量():无效{
这个。索引++;
}
递减():无效{
这个.index--;
}
等于(其他:IRandomAccessIterator<T>):布尔值{
返回 this.index == (<ArrayIterator<T>>other).index;
}
克隆():IRandomAccessIterator<T> {
返回新的 ArrayIterator(this.array, this.index);
}
移动(n:数字):void {
this.index += n; 1
}
distance(other: IRandomAccessIterator<T>): number { 2
return this.index - (<ArrayIterator<T>>other).index;
}
}class ArrayIterator<T> implements IRandomAccessIterator<T> {
private array: T[];
private index: number;
constructor(array: T[], index: number) {
this.array = array;
this.index = index;
}
get(): T {
return this.array[this.index];
}
set(value: T): void {
this.array[this.index] = value;
}
increment(): void {
this.index++;
}
decrement(): void {
this.index--;
}
equals(other: IRandomAccessIterator<T>): boolean {
return this.index == (<ArrayIterator<T>>other).index;
}
clone(): IRandomAccessIterator<T> {
return new ArrayIterator(this.array, this.index);
}
move(n: number): void {
this.index += n; 1
}
distance(other: IRandomAccessIterator<T>): number { 2
return this.index - (<ArrayIterator<T>>other).index;
}
}
让我们采用一个受益于随机访问迭代器的非常简单的算法elementAt():begin该算法将 a和end定义序列和数字 n 的迭代器作为参数。end如果 n 大于序列的长度, 它将返回一个迭代器到序列的第 n 个元素或迭代器。
Let’s take a very simple algorithm that benefits from a random-access iterator: elementAt(). This algorithm takes as arguments a begin and end iterator defining a sequence and a number n. It will return an iterator to the nth element of the sequence or the end iterator if n is larger than the length of the sequence.
我们可以用一个输入迭代器来实现这个算法,但是我们必须将迭代器递增 n 次才能到达元素。那是线性时间复杂度,或 O(n)。使用随机访问迭代器,我们可以在常数时间或 O(1) 内执行此操作,如下一个清单所示。
We can implement this algorithm with an input iterator, but we would have to increment the iterator n times to reach the element. That is linear time complexity, or O(n). With a random-access iterator, we can do this in constant time, or O(1), as shown in the next listing.
函数 elementAtRandomAccessIterator<T>(
开始:IRandomAccessIterator<T>,结束:IRandomAccessIterator<T>,
n: 数字): IRandomAccessIterator<T> {
begin.move(n); 1个
如果(开始。距离(结束)<= 0)返回结束; 2个
回归开始; 3
}function elementAtRandomAccessIterator<T>(
begin: IRandomAccessIterator<T>, end: IRandomAccessIterator<T>,
n: number): IRandomAccessIterator<T> {
begin.move(n); 1
if (begin.distance(end) <= 0) return end; 2
return begin; 3
}
随机访问迭代器支持最高效的算法,但很少有数据结构可以提供这样的迭代器。
Random-access iterators enable the most efficient algorithms, but fewer data structures can provide such iterators.
我们已经研究了各种类别的迭代器,以及它们的不同功能如何支持更高效的算法。我们从输入和输出迭代器开始,它们对序列执行一次遍历。输入迭代器允许我们读取值,而输出迭代器允许我们设置值。
We’ve looked at the various categories of iterators and how their different capabilities enable more efficient algorithms. We started with input and output iterators, which perform a one-pass traversal over a sequence. Input iterators allow us to read values, whereas output iterators allow us to set values.
这就是我们需要的算法,例如map()、filter()和reduce(),它们以线性方式处理它们的输入。大多数编程语言只为这种类型的迭代器提供算法库,包括 Java 和 C#,以及它们的Iterable<T>和IEnumerable<T>.
This is all we need for algorithms such as map(), filter(), and reduce(), which process their input in linear fashion. Most programming languages provide algorithm libraries for only this type of iterator, including Java and C#, with their Iterable<T> and IEnumerable<T>.
接下来,我们看到添加读取和写入值以及创建迭代器副本的能力,可以启用其他可以就地修改数据的有用算法。这些新功能由前向迭代器提供。
Next, we saw that adding the ability to both read and write a value, and to create a copy of an iterator, enables other useful algorithms that can modify data in place. These new capabilities were supplied by a forward iterator.
在某些情况下,例如示例reverse(),仅向前移动序列是不够的。我们需要双向移动。既可以前进也可以后退的迭代器称为双向迭代器。
In some cases, such as the reverse() example, moving only forward through a sequence is not enough. We need to move both ways. An iterator that can step both forward and backward is called a bidirectional iterator.
最后,如果某些算法可以跳过一个序列并访问任意位置的项目而无需逐步遍历,则它们的性能会更好。排序算法就是很好的例子;elementAt()我们刚刚看到的简单也是如此。为了支持这样的算法,我们引入了随机访问迭代器,它可以一步移动多个元素。
Finally, some algorithms perform better if they can jump around a sequence and access items at arbitrary locations without needing to traverse step by step. Sorting algorithms are good examples; so is the simple elementAt() that we just saw. To support such algorithms, we introduced the random-access iterator, which can move over multiple elements in one step.
这些想法并不新鲜;C++ 标准库提供了一组使用具有类似功能的迭代器的高效算法。其他语言将自己限制在较小的算法集或效率较低的实现上。
These ideas are not new; the C++ standard library provides a set of efficient algorithms that use iterators with similar capabilities. Other languages limit themselves to a smaller set of algorithms or less-efficient implementations.
您可能已经注意到基于迭代器的算法并不流畅,因为它们将一对迭代器作为输入并返回其中一个void或一个迭代器。C++ 正在从迭代器转向范围。我们不会在本书中深入讨论这个主题,但在较高的层次上,一个范围可以被认为是一对begin/end迭代器。更新算法以将范围作为参数并返回范围为更流畅的 API 奠定了基础,我们可以在其中对范围进行链式操作。在未来的某个时候,基于范围的算法很可能会进入其他语言。使用功能强大的迭代器在任何数据结构上运行高效、就地、通用算法的能力非常有用。
You may have noticed that the iterator-based algorithms were not fluent, as they took a pair of iterators as input and returned either void or an iterator. C++ is moving from iterators to ranges. We won’t cover this topic deeply in this book, but at a high level, a range can be thought of as a pair of begin/end iterators. Updating the algorithms to take ranges as arguments and to return ranges sets the stage for a more fluent API in which we can chain operations on ranges. It is likely that at some point in the future, range-based algorithms will make their way into other languages. The ability to run efficient, in-place, generic algorithms over any data structure with a capable-enough iterator is extremely useful.
支持drop()跳过范围的前 n 个元素所需的最小迭代器类别是什么?
- InputIterator
- ForwardIterator
- BidirectionalIterator
- RandomAccessIterator
What is the minimum iterator category required to support drop() that skips the first n elements of a range?
- InputIterator
- ForwardIterator
- BidirectionalIterator
- RandomAccessIterator
支持二进制搜索算法(O(log n))所需的最小迭代器类别是什么?提醒一下,二分查找检查范围的中间元素。如果它大于搜索的值,它将范围分成两半并查看前半部分。如果不是,它会查看范围的后半部分,然后重复。这个想法是搜索空间在每一步减半,所以算法的复杂度是 O(log n)。
- InputIterator
- ForwardIterator
- BidirectionalIterator
- RandomAccessIterator
What is the minimum iterator category required to support a binary search algorithm (with O(log n))? As a reminder, binary search checks the middle element of a range. If it’s larger than the value searched for, it splits the range in halves and looks at the first half. If not, it looks at the second half of the range and then repeats. The idea is that the search space is halved at each step, so the complexity of the algorithm is O(log n).
- InputIterator
- ForwardIterator
- BidirectionalIterator
- RandomAccessIterator
我们对迭代器的要求越多,能够提供它的数据结构就越少。我们看到我们可以在单向链表、双向链表或数组上创建前向迭代器。如果我们想要一个双向迭代器,那么单向链表就不适用了。我们可以在双向链表和数组上获得一个双向迭代器,但不能在单链表上获得。如果我们想要一个随机访问迭代器,我们需要删除双向链表。
The more we ask of an iterator, the fewer the data structures that can supply it. We saw that we can create a forward iterator over a singly linked list, a doubly linked list, or an array. If we want a bidirectional iterator, singly linked lists are out of the picture. We can get a bidirectional iterator over doubly linked lists and arrays but not singly linked lists. If we want a random-access iterator, we need to drop doubly linked lists.
我们希望泛型算法尽可能通用,并且它们需要功能最少但足以支持该算法的迭代器。但正如我们刚刚看到的,算法的低效版本对迭代器的要求并不高。对于某些算法,我们可以提供多个版本:使用功能较弱的迭代器的低效版本和使用功能更强的迭代器的效率更高的版本。
We want generic algorithms to be as general as possible, and they require the least capable iterator that is good enough to support the algorithm. But as we just saw, less efficient versions of an algorithm don’t require that much from their iterators. For some algorithms, we can provide multiple versions: a less-efficient version that works with a less-capable iterator and a more-efficient version that works with a more--capable iterator.
让我们回顾一下我们的elementAt()例子。如果 n 大于序列的长度,则此算法将返回序列中的第 n 个值或序列的末尾。如果我们有一个前向迭代器,我们可以递增它 n 次并返回值。这具有线性或 O(n) 的复杂性,因为随着 n 的增加,我们需要执行更多的步骤。另一方面,如果我们有一个随机访问迭代器,我们可以在常量或 O(1) 时间内检索元素。
Let’s revisit our elementAt() example. This algorithm will return the nth value in a sequence or the end of the sequence if n is larger than the length of the sequence. If we have a forward iterator, we can increment it n times and return the value. This has linear, or O(n) complexity, as we need to perform more steps as n increases. On the other hand, if we have a random-access iterator, we can retrieve the element in constant, or O(1), time.
我们是想提供一种更通用、效率更低的算法,还是提供一种限于更少数据结构的更高效的算法?答案是我们不必选择:我们可以提供算法的两个版本,并且根据我们获得的迭代器类型,我们可以利用最有效的实现。
Do we want to provide a more-general, less-efficient algorithm or a more-efficient algorithm that is limited to fewer data structures? The answer is that we don’t have to choose: we can provide two versions of the algorithm, and depending on the type of iterator we get, we can leverage the most-efficient implementation.
让我们实现一个elementAtForwardIterator()以线性时间检索元素的函数和一个elementAtRandomAccessIterator()以常数时间检索元素的函数,如以下清单所示。
Let’s implement an elementAtForwardIterator() that retrieves the element in linear time and an elementAtRandomAccessIterator() that retrieves the element in constant time, as shown in the following listing.
函数 elementAtForwardIterator<T>(
开始: IForwardIterator<T>,结束: IForwardIterator<T>,
n: 数字): IForwardIterator<T> {
while (!begin.equals(end) && n > 0) {
begin.increment(); 1
名词--; 1个
}
回归开始; 2个
}
函数 elementAtRandomAccessIterator<T>( 3
开始:IRandomAccessIterator<T>,结束:IRandomAccessIterator<T>,
n: 数字): IRandomAccessIterator<T> {
begin.move(n);
如果(开始。距离(结束)<= 0)返回结束;
回归开始;
}function elementAtForwardIterator<T>(
begin: IForwardIterator<T>, end: IForwardIterator<T>,
n: number): IForwardIterator<T> {
while (!begin.equals(end) && n > 0) {
begin.increment(); 1
n--; 1
}
return begin; 2
}
function elementAtRandomAccessIterator<T>( 3
begin: IRandomAccessIterator<T>, end: IRandomAccessIterator<T>,
n: number): IRandomAccessIterator<T> {
begin.move(n);
if (begin.distance(end) <= 0) return end;
return begin;
}
现在我们可以实现一个elementAt()算法,它根据作为参数接收的迭代器的能力来选择要应用的算法,如清单 10.40所示。请注意,TypeScript 不支持函数重载,因此我们需要使用一个函数来确定迭代器的类型。在其他语言中,例如 C# 和 Java,我们可以简单地提供具有相同名称但采用不同参数的方法。
Now we can implement an elementAt() that picks the algorithm to apply based on the capabilities of the iterators it receives as arguments, as shown in listing 10.40. Note that TypeScript doesn’t support function overloading, so we need to use a function that determines the type of the iterator. In other languages, such as C# and Java, we can simply provide methods that have the same name but take different arguments.
函数 isRandomAccessIterator<T>(
iter: IForwardIterator<T>): iter 是 IRandomAccessIterator<T> {
在 iter 中返回“距离”; 1个
}
函数 elementAt<T>(
开始: IForwardIterator<T>,结束: IForwardIterator<T>,
n: 数字): IForwardIterator<T> {
如果(isRandomAccessIterator(开始)&& isRandomAccessIterator(结束)){
返回 elementAtRandomAccessIterator(开始,结束,n); 2个
} 别的 {
返回 elementAtForwardIterator(开始,结束,n); 3个
}
}function isRandomAccessIterator<T>(
iter: IForwardIterator<T>): iter is IRandomAccessIterator<T> {
return "distance" in iter; 1
}
function elementAt<T>(
begin: IForwardIterator<T>, end: IForwardIterator<T>,
n: number): IForwardIterator<T> {
if (isRandomAccessIterator(begin) && isRandomAccessIterator(end)) {
return elementAtRandomAccessIterator(begin, end, n); 2
} else {
return elementAtForwardIterator(begin, end, n); 3
}
}
一个好的算法与其所拥有的一起工作;它适应具有较低效率实现的功能较弱的迭代器,同时为功能更强大的迭代器启用最有效的实现。
A good algorithm works with what it has; it adapts to a less-capable iterator with a less-efficient implementation while enabling the most-efficient implementation for more-capable iterators.
Implement nthLast(),一个将迭代器返回到范围的倒数第 n 个元素的函数(如果范围太小,则结束)。如果 n 为 1,我们返回一个指向最后一个元素的迭代器;如果 n 为 2,我们返回指向倒数第二个元素的迭代器,依此类推。如果 n 为 0,我们返回指向范围最后一个元素的结束迭代器。
Implement nthLast(), a function that returns an iterator to the nth-last element of a range (or end if the range is too small). If n is 1, we return an iterator pointing to the last element; if n is 2, we return an iterator pointing to the second to last element, and so on. If n is 0, we return the end iterator pointing one past the last element of the range.
提示:我们可以通过ForwardIterator两次传递来实现这一点。第一遍计算范围内的元素。在第二遍中,因为我们知道范围的大小,所以我们知道什么时候停止到最后的 n 项。
Hint: we can implement this with a ForwardIterator with two passes. The first pass counts the elements of the range. In the second pass, because we know the size of the range, we know when to stop to be n items from the end.
在第 11 章中,我们将把它提升到下一个抽象级别——更高种类的类型——并解释什么是 monad 以及我们可以用它做什么。
In chapter 11, we’ll step it up to the next level of abstraction—higher kinded types—and explain what a monad is and what we can do with it.
reduce()使用and 的可能实现filter():
函数 concatenateNonEmpty(iter: Iterable<string>): string { 返回减少( 筛选( 迭代器, (值) => 值.长度 > 0), "", (str1: string, str2: string) => str1 + str2); }
A possible implementation using reduce() and filter():
function concatenateNonEmpty(iter: Iterable<string>): string { return reduce( filter( iter, (value) => value.length > 0), "", (str1: string, str2: string) => str1 + str2); }
map()使用and 的可能实现filter():
function squareOdds(iter: Iterable<number>): IterableIterator<number> { 返回地图( 筛选( 迭代器, (值) => 值 % 2 == 1), (x) => x * x ); }
A possible implementation using map() and filter():
function squareOdds(iter: Iterable<number>): IterableIterator<number> { return map( filter( iter, (value) => value % 2 == 1), (x) => x * x ); }
一个可能的实现:
类 FluentIterable<T> { /* ... */ 采取(n:数字):FluentIterable<T> { 返回新的 FluentIterable (this.takeImpl(n)); } private *takeImpl(n: number): IterableIterator<T> { for (this.iter 的常量值) { 如果 (n-- <= 0) 返回; 屈服值; } } }
A possible implementation:
class FluentIterable<T> { /* ... */ take(n: number): FluentIterable<T> { return new FluentIterable (this.takeImpl(n)); } private *takeImpl(n: number): IterableIterator<T> { for (const value of this.iter) { if (n-- <= 0) return; yield value; } } }
一个可能的实现:
类 FluentIterable<T> { /* ... */ drop(n: number): FluentIterable<T> { 返回新的 FluentIterable(this.dropImpl(n)); } private *dropImpl(n: number): IterableIterator<T> { for (this.iter 的常量值) { 如果 (n-- > 0) 继续; 屈服值; } } }
A possible implementation:
class FluentIterable<T> { /* ... */ drop(n: number): FluentIterable<T> { return new FluentIterable(this.dropImpl(n)); } private *dropImpl(n: number): IterableIterator<T> { for (const value of this.iter) { if (n-- > 0) continue; yield value; } } }
使用通用类型约束来确保的可能解决方案T是IComparable:
function clamp<T extends IComparable<T>>(value: T, low: T, high: T): T { 如果(value.compareTo(low)== ComparisonResult.LessThan){ 返回低点; } 如果(value.compareTo(high)== ComparisonResult.GreaterThan){ 回报高; } 返回值; }
A possible solution using a generic type constraint to ensure that T is IComparable:
function clamp<T extends IComparable<T>>(value: T, low: T, high: T): T { if (value.compareTo(low) == ComparisonResult.LessThan) { return low; } if (value.compareTo(high) == ComparisonResult.GreaterThan) { return high; } return value; }
a—drop()甚至可以用于潜在的无限数据流。能够简单的进阶就足够了。
a—drop() can be used even on potentially infinite streams of data. Being able simply to advance is sufficient.
d—二分搜索需要能够在每一步都跳到范围的中间才能有效。双向迭代器仍然必须逐个元素地执行以到达范围的一半,这不会使其成为 O(log n)。(一步一步是 O(n) 或线性的。)
d—Binary search needs to be able to jump to the middle of the range at each step to be efficient. A bidirectional iterator would still have to step element by element to reach the half of the range, which would not make it O(log n). (Step by step is O(n) or linear.)
如果自适应算法接收到双向迭代器,它将从后面递减,而当它接收到前向迭代器时,将使用两次遍历方法。这是一个可能的实现:
函数 nthLastForwardIterator<T>( 开始: IForwardIterator<T>,结束: IForwardIterator<T>,n:数字) : IForwardIterator<T> { 让长度:数字= 0; 让 begin2: IForwardIterator<T> = begin.clone(); // 判断范围的长度 while (!begin.equals(end)) { begin.increment(); 长度++; } 如果(长度 < n)返回结束; 让 curr: number = 0; // 前进直到当前元素是倒数第n个 while (!begin2.equals(end) && curr < length - n) { begin2.增量(); 当前++; } 返回开始2; } 函数 nthLastBidirectionalIterator<T>( 开始:IBidirectionalIterator<T>,结束:IBidirectionalIterator<T>,n:数字) : IBidirectionalIterator<T> { 让 curr: IBidirectionalIterator<T> = end.clone(); while (n > 0 && !curr.equals(begin)) { 当前递减(); n--; } // 如果我们在递减 n 次之前到达开始,则范围太小 如果(n > 0)返回结束; 返回电流; } 函数是双向迭代器<T>( iter: IForwardIterator<T>): iter is IBidirectionalIterator<T> { 在 iter 中返回“减量”; } 函数 nthLast<T>( 开始: IForwardIterator<T>,结束: IForwardIterator<T>,n:数字) : IForwardIterator<T> { if (isBidirectionalIterator(begin) && isBidirectionalIterator(end)) { 返回 nthLastBidirectionalIterator(开始,结束,n); } 别的 { 返回 nthLastForwardIterator(开始,结束,n); } }
An adaptive algorithm will decrement from the back if it receives bidirectional iterators and use the two-pass approach when it receives forward iterators. Here is a possible implementation:
function nthLastForwardIterator<T>( begin: IForwardIterator<T>, end: IForwardIterator<T>, n: number) : IForwardIterator<T> { let length: number = 0; let begin2: IForwardIterator<T> = begin.clone(); // Determine the length of the range while (!begin.equals(end)) { begin.increment(); length++; } if (length < n) return end; let curr: number = 0; // Advance until the current element is the nth from the back while (!begin2.equals(end) && curr < length - n) { begin2.increment(); curr++; } return begin2; } function nthLastBidirectionalIterator<T>( begin: IBidirectionalIterator<T>, end: IBidirectionalIterator<T>, n: number) : IBidirectionalIterator<T> { let curr: IBidirectionalIterator<T> = end.clone(); while (n > 0 && !curr.equals(begin)) { curr.decrement(); n--; } // Range is too small if we reached begin before decrementing n times if (n > 0) return end; return curr; } function isBidirectionalIterator<T>( iter: IForwardIterator<T>): iter is IBidirectionalIterator<T> { return "decrement" in iter; } function nthLast<T>( begin: IForwardIterator<T>, end: IForwardIterator<T>, n: number) : IForwardIterator<T> { if (isBidirectionalIterator(begin) && isBidirectionalIterator(end)) { return nthLastBidirectionalIterator(begin, end, n); } else { return nthLastForwardIterator(begin, end, n); } }
本章涵盖
This chapter covers
在整本书中,我们研究了一种非常常见的算法的各种版本,map()在第 10 章中,我们了解了迭代器如何提供一种抽象,使我们能够在各种数据结构中重用它。在本章中,我们将看到如何将这个算法扩展到迭代器之外并提供一个更通用的版本。这种强大的算法允许我们混合和匹配泛型类型和函数,并且可以通过提供统一的方式来处理错误来提供帮助。
Throughout the book, we’ve looked at various versions of a very common algorithm, map(), and in chapter 10 we saw how iterators provide an abstraction that allows us to reuse it across various data structures. In this chapter, we’ll see how we can extend this algorithm beyond iterators and provide an even more general version. This powerful algorithm allows us to mix and match generic types and functions, and can help by providing a uniform way to handle errors.
看完几个例子后,我们将为这个广泛适用的函数族(称为仿函数)提供一个定义。我们还将解释什么是更高种类的类型以及它们如何帮助我们定义此类通用函数。我们将研究我们在使用缺乏对更高种类类型支持的语言时遇到的限制。
After we go over a few examples, we’ll provide a definition for this broadly applicable family of functions, known as functors. We’ll also explain what higher kinded types are and how they help us define such generic functions. We’ll look at the limitations we run into with languages that lack support for higher kinded types.
接下来,我们将看看 monad。这个词出现在多个地方,虽然听起来很吓人,但这个概念很简单。我们将解释什么是 monad 并介绍多个应用程序,从更好的错误传播到异步代码和序列展平。
Next, we’ll look at monads. The term shows up in multiple places, and although it might sound intimidating, the concept is straightforward. We’ll explain what a monad is and go over multiple applications, from better error propagation to asynchronous code and sequence flattening.
我们将以一个部分结束,讨论我们在本书中学到的一些主题,以及我们没有涉及的其他几种类型:依赖类型和线性类型。我们不会在这里详细介绍;相反,我们将提供一个快速摘要并列出一些资源,以供您了解更多信息。我们推荐几本书来详细了解这些主题中的每一个,以及为其中一些功能提供支持的编程语言。
We will wrap up with a section that discusses some of the topics we learned about in this book and a couple of other kinds of types we did not cover: dependent types and linear types. We won’t go into details here; rather, we’ll provide a quick summary and list some resources in case you want to learn more. We recommend several books to learn more about each of these topics, as well as programming languages that provide support for some of these features.
在第 10 章中,我们将仅适用于数组的第 5 章map()的实现更新为适用于迭代器的通用实现,如清单 11.1所示。我们讨论了迭代器如何抽象数据结构遍历,因此我们的新版本可以将函数应用于任何数据结构中的元素(图 11.1)。 map()
In chapter 10, we updated our map() implementation from chapter 5, which worked only on arrays, to a generic implementation that worked on iterators, shown in listing 11.1. We talked about how iterators abstract data structure traversal, so our new version of map() can apply a function to elements in any data structure (figure 11.1).
function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):
IterableIterator<U> {
for (iter 的常量值) {
收益函数(值);
}
}function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):
IterableIterator<U> {
for (const value of iter) {
yield func(value);
}
}
此实现适用于迭代器,但我们也应该能够将表单的函数应用于(item: T) => U其他类型。让我们以我们在第 3 章Optional<T>中定义的类型为例,如下一个清单所示。
This implementation works on iterators, but we should be able to apply a function of the form (item: T) => U to other types too. Let’s take, as an example, the Optional<T> type we defined in chapter 3, shown in the next listing.
可选类 <T> {
私有值:T | 不明确的;
私人分配:布尔值;
构造函数(值?:T){
如果(值){
this.value = 值;
this.assigned = true;
} 别的 {
this.value = undefined;
this.assigned = false;
}
}
有值():布尔值{
返回this.assigned;
}
getValue(): T {
如果(!this.assigned)抛出错误();
返回<T>这个值;
}
}class Optional<T> {
private value: T | undefined;
private assigned: boolean;
constructor(value?: T) {
if (value) {
this.value = value;
this.assigned = true;
} else {
this.value = undefined;
this.assigned = false;
}
}
hasValue(): boolean {
return this.assigned;
}
getValue(): T {
if (!this.assigned) throw Error();
return <T>this.value;
}
}
能够将函数映射(value: T) => U到Optional<T>. 如果 optional 包含 type 的值T,则将函数映射到它上面应该返回一个Optional<U>包含应用该函数的结果的 。另一方面,如果可选不包含值,映射将导致空Optional<U>(图 11.2)。
It feels natural to be able to map a function (value: T) => U over an Optional<T>. If the optional contains a value of type T, mapping the function over it should return an Optional<U> containing the result of applying the function. On the other hand, if the optional doesn’t contain a value, mapping would result in an empty Optional<U> (figure 11.2).
让我们勾勒出一个实现。我们将把这个函数放在一个命名空间中。因为 TypeScript 不支持函数重载,所以要有多个同名的函数,我们需要将它们放在不同的命名空间中,以便编译器可以确定我们正在调用的函数。
Let’s sketch out an implementation. We’ll put this function in a namespace. Because TypeScript doesn’t support function overloading, to have multiple functions with the same name, we need to put them in different namespaces so the compiler can determine the function we are calling.
命名空间可选{
导出函数 map<T, U>( 1
可选:可选<T>,功能:(值:T)=> U):可选<U> {
如果(可选。hasValue()){
返回新的 Optional<U>(func(optional.getValue())); 2个
} 别的 {
返回新的可选<U>(); 3个
}
}
}namespace Optional {
export function map<T, U>( 1
optional: Optional<T>, func: (value: T) => U): Optional<U> {
if (optional.hasValue()) {
return new Optional<U>(func(optional.getValue())); 2
} else {
return new Optional<U>(); 3
}
}
}
我们可以用 TypeScript sum 类型T或做一些非常相似的事情undefined。请记住,Optional<T>是这种类型的 DIY 版本,即使在本机不支持求和类型的语言中也能工作,但 TypeScript 支持。让我们看看如何映射“本地”可选类型T | undefined。
We can do something very similar with the TypeScript sum type T or undefined. Remember, Optional<T> is a DIY version of such a type that works even in languages that don’t support sum types natively, but TypeScript does. Let’s see how we can map over a “native” optional type T | undefined.
(value: T) => U如果我们有一个类型的值,则映射一个函数T | undefined应该应用该函数并返回它的结果T,或者undefined如果我们以undefined.
Mapping a function (value: T) => U over T | undefined should apply the function and return its result if we have a value of type T, or return undefined if we start with undefined.
命名空间 SumType {
导出函数 map<T, U>(
值:T | 未定义,函数:(值:T)=> U):U | 不明确的 {
如果(值==未定义){
返回未定义;
} 别的 {
返回函数(值);
}
}
}namespace SumType {
export function map<T, U>(
value: T | undefined, func: (value: T) => U): U | undefined {
if (value == undefined) {
return undefined;
} else {
return func(value);
}
}
}
map()这些类型不能被迭代,但为它们存在一个函数仍然有意义。让我们定义另一个简单的泛型类型, ,Box<T>如以下清单所示。这种类型只是包装了一个 type 的值T。
These types can’t be iterated over, but it still makes sense for a map() function to exist for them. Let’s define another simple generic type, Box<T>, shown in the following listing. This type simply wraps a value of type T.
类框 <T> {
值:T; 1个
构造函数(值:T){
this.value = 值;
}
}class Box<T> {
value: T; 1
constructor(value: T) {
this.value = value;
}
}
(value: T) => U我们可以在这种类型上映射一个函数吗?我们可以。正如您可能已经猜到的那样,map()forBox<T>会返回 a :它将从 中取出Box<U>值,对其应用函数,然后将结果放回 a ,如图11.3和清单 11.6所示。 TBox<T>Box<U>
Can we map a function (value: T) => U over this type? We can. As you might have guessed, map() for Box<T> would return a Box<U>: it will take the value T out of Box<T>, apply the function to it, and put the result back into a Box<U>, as shown in figure 11.3 and listing 11.6.
命名空间框 {
导出函数 map<T, U>(
box: Box<T>, func: (value: T) => U): Box<U> {
返回新的 Box<U>(func(box.value)); 1个
}
}namespace Box {
export function map<T, U>(
box: Box<T>, func: (value: T) => U): Box<U> {
return new Box<U>(func(box.value)); 1
}
}
我们可以将函数映射到许多泛型类型上。为什么此功能有用?它很有用map(),因为与迭代器一样,它提供了另一种方法来将存储数据的类型与操作该数据的函数分离。
We can map functions over many generic types. Why is this capability useful? It’s useful because map(), like iterators, provides another way to decouple types that store data from functions that operate on that data.
作为一个具体的例子,我们来看几个处理数值的函数。我们将实现一个简单的square()函数,该函数将数字作为参数并返回它的方块。我们还将实现stringify()一个以数字作为参数并返回其字符串表示形式的函数,如下一个清单所示。
As a concrete example, let’s take a couple of functions that process a numerical value. We’ll implement a simple square(), a function that takes a number as an argument and returns its square. We’ll also implement stringify(), a function that takes a number as an argument and returns its string representation, as shown in the next listing.
函数平方(值:数字):数字{
返回值** 2;
}
函数stringify(值:数字):字符串{
返回值.toString();
}function square(value: number): number {
return value ** 2;
}
function stringify(value: number): string {
return value.toString();
}
现在假设我们有一个readNumber()函数,它从文件中读取一个数值,如清单 11.8所示。因为我们正在处理输入,所以我们可能会遇到一些问题。例如,如果文件不存在或无法打开怎么办?在这种情况下,read-Number()将返回undefined. 我们不会看这个函数的实现;对于我们的例子来说重要的是它的返回类型。
Now let’s say that we have a readNumber() function, which reads a numeric value from a file, as shown in listing 11.8. Because we are dealing with input, we might run into some problems. What if the file doesn’t exist or can’t be opened, for example? In that case, read-Number() will return undefined. We won’t look at the implementation of this function; the important thing for our example is its return type.
函数 readNumber():数字 | 不明确的 {
/* 省略实现 */
}function readNumber(): number | undefined {
/* Implementation omitted */
}
如果我们想读取一个数字并通过square()首先应用它来处理它,然后再应用它stringify(),我们需要确保我们实际上有一个数值而不是undefined。一种可能的实现是将 from 转换number | undefined为number,if在需要的地方使用语句,如下一个清单所示。
If we want to read a number and process it by applying square() to it first and then stringify(), we need to ensure that we actually have a numerical value as opposed to undefined. A possible implementation is to convert from number | undefined to number, using if statements wherever needed, as the next listing shows.
函数过程():字符串| 不明确的 {
让值:数字| undefined = readNumber();
如果(值==未定义)返回未定义; 1个
返回 stringify(square(value)); 2
}function process(): string | undefined {
let value: number | undefined = readNumber();
if (value == undefined) return undefined; 1
return stringify(square(value)); 2
}
我们有两个对数字进行操作的函数,但是因为我们的输入也可以是undefined,所以我们需要明确地处理这种情况。这并不是特别糟糕,但一般来说,我们的代码分支越少,它就越不复杂。它更容易理解和维护,出现错误的机会也更少。另一种看待这个问题的方法是它process()本身只是传播undefined;它对它没有任何用处。process()如果我们能继续负责就更好了处理并让其他人处理错误情况。我们应该怎么做?我们map()实现了求和类型,如下面的清单所示。
We have two functions that operate on numbers, but because our input can also be undefined, we need to handle that case explicitly. This is not particularly bad, but in general, the less branching our code has, the less complex it is. It is easier to understand and to maintain, and there are fewer opportunities for bugs. Another way to look at this is that process() itself simply propagates undefined; it doesn’t do anything useful with it. It would be better if we could keep process() responsible for processing and let someone else handle error cases. How can we do this? With the map() we implemented for sum types, as shown in the following listing.
命名空间 SumType {
导出函数 map<T, U>( 1
值:T | 未定义,函数:(值:T)=> U):U | 不明确的 {
如果(值==未定义){
返回未定义;
} 别的 {
返回函数(值);
}
}
}
函数过程():字符串| 不明确的 {
让值:数字| undefined = readNumber();
让平方值:数字| 未定义 =
SumType.map(value, square); 2个
返回 SumType.map(squaredValue, stringify); 3
}namespace SumType {
export function map<T, U>( 1
value: T | undefined, func: (value: T) => U): U | undefined {
if (value == undefined) {
return undefined;
} else {
return func(value);
}
}
}
function process(): string | undefined {
let value: number | undefined = readNumber();
let squaredValue: number | undefined =
SumType.map(value, square); 2
return SumType.map(squaredValue, stringify); 3
}
现在我们的process()实现没有分支。number | undefined解包到 anumber和检查的责任undefined由map(). map()是通用的,可以跨许多其他类型(例如string | undefined)和许多其他处理功能使用。
Now our process() implementation has no branching. The responsibility for unpacking number | undefined into a number and checking for undefined is handled by map(). map() is generic and can be used across many other types (such as string | undefined) and in many other processing functions.
在我们的例子中,因为square()保证返回一个数字,我们可以创建一个链接square()and的小 lambda stringify(),并将其传递给map()下一个清单。
In our case, because square() is guaranteed to return a number, we can create a small lambda that chains square() and stringify(), and pass that to map() in the next listing.
函数过程():字符串| 不明确的 {
让值:数字| undefined = readNumber();
返回 SumType.map(值,
(值: 数字) => stringify(square(value)) ); 1
}function process(): string | undefined {
let value: number | undefined = readNumber();
return SumType.map(value,
(value: number) => stringify(square(value))); 1
}
此实现是 的功能实现process(),因为错误传播被委托给map()。我们将在讨论 monad 的 11.2 节中更多地讨论错误处理。现在,让我们看一下map().
This implementation is a functional implementation of process(), in that the error propagation is delegated to map(). We’ll talk more about error handling in section 11.2, which discusses monads. For now, let’s look at another application of map().
如果没有map()函数族,如果我们有一个square()平方 a 的函数number,我们将不得不实现一些额外的逻辑来number从number | undefined求和类型中获取 a 。同样,我们必须实现一些额外的逻辑来从 a 中获取值Box<number>并将其打包回 a 中Box-<number>,如以下清单所示。
Without the map() family of functions, if we have a square() function that squares a number, we would have to implement some additional logic to get a number from a number | undefined sum type. Similarly, we would have to implement some additional logic to get a value from a Box<number> and package it back in a Box-<number>, as the following listing shows.
函数 squareSumType(值:数字 | 未定义) 1
: 号码 | 不明确的 {
如果(值==未定义)返回未定义;
返回平方(值);
}
function squareBox(box: Box<number>): Box<number> { 2
返回新框(正方形(box.value));
}function squareSumType(value: number | undefined) 1
: number | undefined {
if (value == undefined) return undefined;
return square(value);
}
function squareBox(box: Box<number>): Box<number> { 2
return new Box(square(box.value));
}
到目前为止,这还不算太糟糕。但是如果我们想要类似的东西怎么办stringify()?同样,我们最终将编写两个看起来很像前面的函数的函数,如以下代码所示。
So far, this isn’t too bad. But what if we want something similar with stringify()? Again, we’ll end up writing two functions that look a lot like the previous ones, as shown in the following code.
函数 stringifySumType(值:数字 | 未定义)
: 串 | 不明确的 {
如果(值==未定义)返回未定义;
返回字符串化(值);
}
function stringifyBox(box: Box<number>): Box<string> {
返回新框(stringify(box.value))
}function stringifySumType(value: number | undefined)
: string | undefined {
if (value == undefined) return undefined;
return stringify(value);
}
function stringifyBox(box: Box<number>): Box<string> {
return new Box(stringify(box.value))
}
这开始看起来像重复代码,这从来都不是好事。如果我们有map()可用于number | undefined和 的函数Box,它们将提供抽象以删除重复代码。我们可以在下一个列表中传递 either square()or stringify()to either SumType.map()or to ;Box.map()不需要额外的代码。
This starts to look like duplicate code, which is never good. If we have map() functions available for number | undefined and Box, they provide the abstraction to remove the duplicate code. We can pass either square() or stringify() to either SumType.map() or to Box.map() in the next listing; no additional code is needed.
让 x: 数字 | 未定义= 1; 让 y: Box<number> = new Box(42); console.log(SumType.map(x, stringify)); console.log(Box.map(y, stringify)); console.log(SumType.map(x, square)); console.log(Box.map(y, square));
let x: number | undefined = 1; let y: Box<number> = new Box(42); console.log(SumType.map(x, stringify)); console.log(Box.map(y, stringify)); console.log(SumType.map(x, square)); console.log(Box.map(y, square));
Now let’s define this family of map() functions.
上一节我们讲的是函子。
What we talked about in the preceding section are functors.
仿函数是执行映射操作的函数的概括。对于像 的任何泛型类型Box<T>,从tomap()获取 aBox<T>和一个函数并生成 a 的操作是一个函子(图 11.4)。 TUBox<U>
A functor is a generalization of functions that perform mapping operations. For any generic type like Box<T>, a map() operation that takes a Box<T> and a function from T to U and produces a Box<U> is a functor (figure 11.4).
函子是非常强大的概念,但大多数主流语言都没有很好的方式来表达它们,因为函子的一般定义依赖于更高种类的类型。
Functors are extremely powerful concepts, but most mainstream languages do not have a good way to express them because the general definition of a functor relies on higher kinded types.
泛型类型是具有类型参数的类型,例如泛型类型T,或类似Box<T>具有类型参数的类型T。高阶类型,就像高阶函数一样,表示一个类型参数和另一个类型参数。T<U>或者Box<T<U>>,例如,有一个类型参数 T ,而 T 又有一个类型参数U。
A generic type is a type that has a type parameter, such as a generic type T, or a type like Box<T> that has a type parameter T. A higher kinded type, just like a higher-order function, represents a type parameter with another type parameter. T<U> or Box<T<U>>, for example, have a type parameter T that in turn has a type parameter U.
在类型系统中,我们可以将类型构造函数视为返回类型的函数。这不是我们自己会实施的东西;这就是类型系统在内部看待类型的方式。
In type systems, we can consider a type constructor to be a function that returns a type. This is not something that we would implement ourselves; this is how the type system looks at types internally.
每种类型都有一个构造函数。一些构造函数是微不足道的。类型的构造函数number可以被认为是一个不带参数并返回类型的函数number。这将是() -> [number type]。
Every type has a constructor. Some constructors are trivial. The constructor for the type number can be thought of as a function that takes no arguments and returns the type number. This would be () -> [number type].
square()即使是具有该类型的函数(例如)(value: number) => number也仍然具有不带参数的类型构造函数() -> [(value: number) => number type],因为即使该函数带有参数,但其类型却没有;它总是一样的。
Even a function, such as square(), that has the type (value: number) => number still has a type constructor with no arguments () -> [(value: number) => number type] because even though the function takes an argument, its type doesn’t; it’s always the same.
当我们谈到泛型时,事情会变得更有趣。泛型类型(例如T[])确实需要实际类型参数来生成具体类型。它的类型构造函数是(T) -> [T[] type]. 例如,当Tis 时number,我们得到一个数字数组number[]作为我们的类型,但是当Tis 时string,我们得到一个字符串数组类型string[]。这样的构造函数也称为种类——即类型的种类T[]。
Things get more interesting when we get to generics. A generic type, such as T[], does need an actual type parameter to produce a concrete type. Its type constructor is (T) -> [T[] type]. When T is number, for example, we get an array of numbers number[] as our type, but when T is string, we get an array of strings type string[]. Such a constructor is also called a kind—that is, the kind of types T[].
更高种类的类型,如高阶函数,将事物提升了一个层次。在这种情况下,我们的类型构造函数可以将另一个类型构造函数作为参数。让我们以 type 为例T<U>[],它是某种类型的数组T,也有一个 type argument U。我们的第一个类型构造函数接受 aU并生成 a T<U>。我们需要将它传递给T<U>[]从它生成的第二个类型的构造函数((U) -> [T<U> type]) -> [T<U>[] type]。
Higher kinded types, like higher-order functions, take things one level up. In this case, our type constructor can take another type constructor as an argument. Let’s take the type T<U>[], which is an array of some type T that also has a type argument U. Our first type constructor takes a U and produces a T<U>. We need to pass this to a second type constructor that produces T<U>[] from it ((U) -> [T<U> type]) -> [T<U>[] type].
正如高阶函数是将其他函数作为参数的函数一样,高阶类型是将其他类型作为参数的类型(参数化类型构造函数)。
Just as higher-order functions are functions that take other functions as argument, higher kinded types are kinds (parameterized type constructors) that take other kinds as arguments.
从理论上讲,我们可以深入到任意数量的级别,例如T<U<V<W>>>,但实际上,在第一T<U>级之后,事情变得不那么有用了。
In theory, we can go any number of levels deep to something like T<U<V<W>>>, but in practice, things become less useful after the first T<U> level.
因为我们没有在 TypeScript、C# 或 Java 中表达更高种类类型的好方法,所以我们无法通过使用类型系统来表达函子来定义构造。Haskell 和 Idris 等具有更强大类型系统的语言使这些定义成为可能。但是,在我们的例子中,因为我们无法通过类型系统强制执行此功能,所以我们可以将其更多地视为一种模式。
Because we don’t have a good way to express higher kinded types in TypeScript, C#, or Java, we can’t define a construct by using the type system to express a functor. Languages such as Haskell and Idris, which have more powerful type systems, make these definitions possible. In our case, though, because we can’t enforce this capability through the type system, we can think of it as more of a pattern.
我们可以说仿函数是具有类型参数T( H<T>) 的任何类型 H ,我们有一个函数map()接受一个类型的参数H<T>和一个函数从Tto U,并返回一个类型的值H<U>。
We can say that a functor is any type H with a type parameter T (H<T>) for which we have a function map() that takes an argument of type H<T> and a function from T to U, and returns a value of type H<U>.
或者,如果我们想要更加面向对象,我们可以创建map()一个成员函数,H<T>如果它有一个方法从tomap()获取一个函数并返回一个类型的值,那么它就是一个仿函数。要准确了解类型系统的不足之处,我们可以尝试为它绘制一个接口。让我们调用此接口 并在下一个清单中 声明它。TUH<U>Functormap()
Alternatively, if we want to be more object-oriented, we can make map() a member function and say that H<T> is a functor if it has a method map() that takes a function from T to U and returns a value of type H<U>. To see exactly where the type system is lacking, we can try to sketch out an interface for it. Let’s call this interface Functor and have it declare map() in the next listing.
接口函子 <T> {
map<U>(func: (value: T) => U): Functor<U>;
}interface Functor<T> {
map<U>(func: (value: T) => U): Functor<U>;
}
We can update Box<T> to implement this interface in the following listing.
类 Box<T> 实现 Functor<T> {
值:T;
构造函数(值:T){
this.value = 值;
}
map<U>(func: (value: T) => U): Box<U> {
返回新框(函数(this.value));
}
}class Box<T> implements Functor<T> {
value: T;
constructor(value: T) {
this.value = value;
}
map<U>(func: (value: T) => U): Box<U> {
return new Box(func(this.value));
}
}
这段代码编译;唯一的问题是它不够具体。调用map()onBox<T>返回一个 type 的实例Box<U>。但是如果我们使用Functor接口,我们会看到map()声明指定它返回 a Functor<U>,而不是 a Box<U>。这不够具体。当我们声明接口时,我们需要一种方法来指定 的返回类型map()(在本例中为Box<U>)。
This code compiles; the only problem is that it isn’t specific enough. Calling map() on Box<T> returns an instance of type Box<U>. But if we work with Functor interfaces, we see that the map() declaration specifies that it returns a Functor<U>, not a Box<U>. This isn’t specific enough. We need a way to specify, when we declare the interface, exactly what the return type of map() will be (in this case, Box<U>).
我们希望能够说,“这个接口将由一个H带有类型参数的类型来实现T。” 以下代码显示了如果 TypeScript 支持更高种类的类型,此声明的外观。它显然无法编译。
We would like to be able to say, “This interface will be implemented by a type H with a type argument T.” The following code shows how this declaration would look like if TypeScript supported higher kinded types. It obviously doesn’t compile.
接口仿函数<H<T>> {
映射<U>(函数:(值:T)=> U):H<U>;
}
类 Box<T> 实现 Functor<Box<T>> {
值:T;
构造函数(值:T){
this.value = 值;
}
map<U>(func: (value: T) => U): Box<U> {
返回新框(函数(this.value));
}
}interface Functor<H<T>> {
map<U>(func: (value: T) => U): H<U>;
}
class Box<T> implements Functor<Box<T>> {
value: T;
constructor(value: T) {
this.value = value;
}
map<U>(func: (value: T) => U): Box<U> {
return new Box(func(this.value));
}
}
缺少这一点,让我们将我们的map()实现视为将函数应用于某些框中的泛型类型或值的模式。
Lacking this, let’s just think of our map() implementations as a pattern for applying functions to generic types or values in some box.
请注意,我们还有函数之上的函子。给定一个具有任意数量参数并返回 type 值的函数 T,我们可以映射一个接受 aT并U在其上生成 a 的函数,最终得到一个接受与原始函数相同的输入并返回 type 值的函数U。在本例中是简单的函数组合,如图11.5map()所示。
Note that we also have functors over functions. Given a function with any number of arguments that returns a value of type T, we can map a function that takes a T and produces a U over it, ending up with a function that takes the same inputs as the original function and returns a value of type U. map() in this case is simply function composition as shown in figure 11.5.
作为示例,让我们使用一个函数,该函数接受两个 type 的参数T并生成一个 type 的值T,并map()在下一个清单中实现其对应的函数。这将返回一个函数,该函数接受两个类型的参数T并返回一个类型的值U。
As an example, let’s take a function that takes two arguments of type T and produces a value of type T, and implement its corresponding map() in the next listing. This returns a function that takes two arguments of type T and returns a value of type U.
命名空间函数{
导出函数 map<T, U>(
f: (arg1: T, arg2: T) => T, func: (value: T) => U) 1
: (arg1: T, arg2: T) => U { 2
返回 (arg1: T, arg2: T) => func(f(arg1, arg2)); 3个
}
}namespace Function {
export function map<T, U>(
f: (arg1: T, arg2: T) => T, func: (value: T) => U) 1
: (arg1: T, arg2: T) => U { 2
return (arg1: T, arg2: T) => func(f(arg1, arg2)); 3
}
}
让我们stringify()映射一个add()接受两个数字并返回它们之和的函数。结果是一个函数,它接受两个数字并返回一个string— 将两个数字相加的字符串化结果,如以下清单所示。
Let’s map stringify() over an add() function that takes two numbers and returns their sum. The result is a function that takes two numbers and returns a string—the stringified result of adding the two numbers, as shown in the following listing.
函数添加(x:数字,y:数字):数字{ 1
返回 x + y;
}
function stringify(value: number): 字符串 { 2
返回值.toString();
}
const 结果:string = Function.map(add, stringify)(40, 2); 3个function add(x: number, y: number): number { 1
return x + y;
}
function stringify(value: number): string { 2
return value.toString();
}
const result: string = Function.map(add, stringify)(40, 2); 3
在仿函数之后,我们将介绍最后一个构造:monad。
After functors, we’ll cover one final construct: the monad.
我们有一个IReader<T>定义单个方法的接口,read(): T. 实现一个仿函数,将函数映射(value: T) => U到 an 上IReader<T>并返回IReader<U>.
We have an interface IReader<T> that defines a single method, read(): T. Implement a functor that maps a function (value: T) => U over an IReader<T> and returns an IReader<U>.
您可能听说过monad这个词,因为它最近受到了很多关注。Monad 正在进入主流编程,因此当您看到它时应该知道它。在第 11.1 节的基础上,在本节中,我们将解释什么是 monad 以及它的用途。我们将从几个示例开始,然后查看一般定义。
You have probably heard the term monad, as it’s been getting a lot of attention lately. Monads are making their way into mainstream programming, so you should know one when you see it. Building on top of section 11.1, in this section we will explain what a monad is and how it is useful. We’ll start with a few examples and then look at the general definition.
在 11.1 节中,我们有一个readNumber()返回 的函数number | undefined。我们使用仿函数来对处理进行排序square(),stringify()因此如果readNumber()返回undefined,则不会发生任何处理,并且undefined通过管道传播。
In section 11.1, we had a readNumber() function that returned number | undefined. We used functors to sequence processing with square() and stringify(), so that if readNumber() returns undefined, no processing happens, and the undefined is propagated through the pipeline.
只要第一个函数——在本例中——readNumber()可以返回错误,这种类型的排序就适用于函子。但是,如果我们想要链接的任何函数可能出错,会发生什么?假设我们要打开一个文件,将其内容作为字符串读取,然后将该字符串反序列化为一个对象,如清单 11.20Cat所示。
This type of sequencing works with functors as long as only the first function—in this case, readNumber()—can return an error. But what happens if any of the functions we want to chain can error out? Let’s say that we want to open a file, read its content as a string, and then deserialize that string into a Cat object, as shown in listing 11.20.
我们有一个openFile()返回 anError或 a 的函数FileHandle。如果文件不存在,如果它被另一个进程锁定,或者如果用户没有打开它的权限,就会发生错误。如果操作成功,我们将返回文件的句柄。
We have an openFile() function that returns an Error or a FileHandle. Errors can occur if the file doesn’t exist, if it is locked by another process, or if the user doesn’t have permission to open it. If the operation succeeds, we get back a handle to the file.
我们有一个readFile()接受 aFileHandle并返回 anError或 a 的函数string。如果无法读取文件,可能会出现错误,这可能是由于太大而无法放入内存。如果可以读取文件,我们会返回一个string.
We have a readFile() function that takes a FileHandle and returns either an Error or a string. Errors can occur if the file can’t be read, perhaps due to being too large to fit in memory. If the file can be read, we get back a string.
最后,deserializeCat()函数接受一个字符串并返回一个Error或一个Cat实例。如果无法将字符串反序列化为Cat对象,可能会出现错误,这可能是由于缺少属性。
Finally, deserializeCat() function takes a string and returns an Error or a Cat instance. Errors can occur if the string can’t be deserialized into a Cat object, perhaps due to missing properties.
所有这些函数都遵循第 3 章中的“返回结果或错误”模式,即从函数返回有效结果或错误,但不能同时返回两者。返回类型将是一个Either<Error, ...>.
All these functions follow the “return result or error” pattern from chapter 3, which suggests returning either a valid result or an error from a function, but not both. The return type will be an Either<Error, ...>.
声明函数 openFile(path: string): Either<Error, FileHandle>; 1个
声明函数 readFile(handle: FileHandle): Either<Error, string>; 2个
声明函数 deserializeCat(
serializedCat: 字符串): Either<Error, Cat>; 3个declare function openFile(path: string): Either<Error, FileHandle>; 1
declare function readFile(handle: FileHandle): Either<Error, string>; 2
declare function deserializeCat(
serializedCat: string): Either<Error, Cat>; 3
我们省略了实现,因为它们并不重要。让我们也快速回顾一下下一个清单中 Either第3 章的实现。
We are omitting the implementations, as they are not important. Let’s also quickly review the implementation of Either from chapter 3 in the next listing.
类 Either<TLeft, TRight> {
私有只读值:TLeft | 对; 1
个私有只读左:布尔值; 1个
私有构造函数(值:TLeft | TRight,左:布尔值){ 2
this.value = 值;
this.left = 左;
}
isLeft(): 布尔值 {
返回 this.left;
}
getLeft(): TLeft { 3
如果(!this.isLeft())抛出新的错误();
返回 <TLeft>this.value;
}
isRight(): 布尔值 {
返回 !this.left;
}
getRight(): TRight { 3
if (this.isRight()) throw new Error();
返回 <TRight>this.value;
}
static makeLeft<TLeft, TRight>(value: TLeft) { 4
返回新的 Either<TLeft, TRight>(value, true);
}
static makeRight<TLeft, TRight>(value: TRight) { 4
返回新的 Either<TLeft, TRight>(value, false);
}
}class Either<TLeft, TRight> {
private readonly value: TLeft | TRight; 1
private readonly left: boolean; 1
private constructor(value: TLeft | TRight, left: boolean) { 2
this.value = value;
this.left = left;
}
isLeft(): boolean {
return this.left;
}
getLeft(): TLeft { 3
if (!this.isLeft()) throw new Error();
return <TLeft>this.value;
}
isRight(): boolean {
return !this.left;
}
getRight(): TRight { 3
if (this.isRight()) throw new Error();
return <TRight>this.value;
}
static makeLeft<TLeft, TRight>(value: TLeft) { 4
return new Either<TLeft, TRight>(value, true);
}
static makeRight<TLeft, TRight>(value: TRight) { 4
return new Either<TLeft, TRight>(value, false);
}
}
现在让我们在下一个清单中看看我们如何将这些函数链接在一起成为一个readCatFromFile()函数,该函数将文件路径作为参数并返回一个Cat实例,或者Error如果在此过程中出现任何错误。
Now let’s see in the next listing how we could chain these functions together into a readCatFromFile() function that takes a file path as an argument and returns a Cat instance, or an Error if anything went wrong along the way.
function readCatFromFile(path: string): Either<Error, Cat> { 1
let handle: Either<Error, FileHandle> = openFile(path); 2个
如果 (handle.isLeft()) 返回 Either.makeLeft(handle.getLeft()); 3个
让内容:Either<Error, string> = readFile(handle.getRight()); 4个
如果 (content.isLeft()) 返回 Either.makeLeft(content.getLeft()); 5个
返回 deserializeCat(content.getRight()); 6
}function readCatFromFile(path: string): Either<Error, Cat> { 1
let handle: Either<Error, FileHandle> = openFile(path); 2
if (handle.isLeft()) return Either.makeLeft(handle.getLeft()); 3
let content: Either<Error, string> = readFile(handle.getRight()); 4
if (content.isLeft()) return Either.makeLeft(content.getLeft()); 5
return deserializeCat(content.getRight()); 6
}
process()这个函数与本章前面的第一个实现非常相似。在那里,我们提供了一个更新的实现,从函数中删除了所有分支和错误检查,并将这些任务委托给map(). 让我们看看代码清单 11.23map()中的forEither<TLeft, TRight>是什么样子的。我们将遵循“对的就是对的;对的就是对的;left is error”,这意味着它包含一个错误,所以只会传播它。仅当包含 a时才会应用给定的函数。 TLeftmap()map()EitherTRight
This function is very similar to the first implementation of process() earlier in this chapter. There, we provided an updated implementation that removed all the branching and error checking from the function and delegated those tasks to map(). Let’s see what a map() for Either<TLeft, TRight> would look like in listing 11.23. We will follow the convention “Right is right; left is error,” which means that TLeft contains an error, so map() will just propagate it. map() will apply a given function only if the Either contains a TRight.
命名空间要么{
导出函数 map<TLeft, TRight, URight>(
值:<TLeft, TRight>,
func: (value: TRight) => URight): Either<TLeft, URight> { 1
if (value.isLeft()) return Either.makeLeft(value.getLeft()); 2个
返回 Either.makeRight(func(value.getRight())); 3个
}
}namespace Either {
export function map<TLeft, TRight, URight>(
value: Either<TLeft, TRight>,
func: (value: TRight) => URight): Either<TLeft, URight> { 1
if (value.isLeft()) return Either.makeLeft(value.getLeft()); 2
return Either.makeRight(func(value.getRight())); 3
}
}
但是,使用 有一个问题map():它期望作为参数的函数类型与我们正在使用的函数不兼容。使用map(),在我们调用openFile()并取回一个之后Either<Error, FileHandle>,我们需要一个函数(value: FileHandle) => string来读取它的内容。该函数本身不能返回Error, likesquare()或stringify()。但在我们的例子中,readFile()可能会失败,所以它不会返回string;它返回Either<Error, string>。如果我们尝试在我们的 中使用它readCatFromFile(),就会出现编译错误,如下一个清单所示。
There is a problem with using map(), though: the types of the functions it expects as arguments are incompatible with the functions we are using. With map(), after we call openFile() and get back an Either<Error, FileHandle>, we would need a function (value: FileHandle) => string to read its content. That function can’t itself return an Error, like square() or stringify(). But in our case, readFile() can fail, so it doesn’t return string; it returns Either<Error, string>. If we attempt to use it in our readCatFromFile(), we get a compilation error, as the next listing shows.
函数 readCatFromFile(path: string): Either<Error, Cat> {
让 handle: Either<Error, FileHandle> = openFile(path);
让内容:Either<Error, string> = Either.map(handle, readFile); 1个
/* ... */
}function readCatFromFile(path: string): Either<Error, Cat> {
let handle: Either<Error, FileHandle> = openFile(path);
let content: Either<Error, string> = Either.map(handle, readFile); 1
/* ... */
}
我们得到的错误信息是
The error message we get is
类型 'Either<Error, Either<Error, string>>' 不是 可分配给类型“Either<Error, string>”。
Type 'Either<Error, Either<Error, string>>' is not assignable to type 'Either<Error, string>'.
我们的仿函数在这里不足。仿函数可以通过处理管道传播初始错误,但如果管道中的每个步骤都可能失败,仿函数将不再工作。在图11.6中,黑色方块代表一个Error,白圈和黑圈代表两种类型,比如FileHandle和string。
Our functor falls short here. Functors can propagate an initial error through the processing pipeline, but if every step in the pipeline can fail, functors no longer work. In figure 11.6, the black square represents an Error, and the white and black circles represent two types, such as FileHandle and string.
map()fromEither<Error, FileHandle>需要一个函数 from File-Handletostring来生成一个Either<Error, string>. readFile()另一方面,我们的功能是从FileHandle到Either<Error, string>。
map() from Either<Error, FileHandle> would need a function from File-Handle to string to produce an Either<Error, string>. Our readFile() function, on the other hand, is from FileHandle to Either<Error, string>.
这个问题很容易解决。我们需要一个类似于map()from Tto的函数Either<Error, U>,如下一个清单所示。这种函数的标准名称是bind().
This problem is easy to fix. We need a function similar to map() that goes from T to Either<Error, U>, as shown in the next listing. The standard name for such a function is bind().
命名空间要么{
导出函数绑定<TLeft, TRight, URight>(
值:<TLeft, TRight>,
func: (值: TRight) => Either<TLeft, URight> 1
): Either<TLeft, URight> {
如果 (value.isLeft()) 返回 Either.makeLeft(value.getLeft());
返回 func(value.getRight()); 2个
}
}namespace Either {
export function bind<TLeft, TRight, URight>(
value: Either<TLeft, TRight>,
func: (value: TRight) => Either<TLeft, URight> 1
): Either<TLeft, URight> {
if (value.isLeft()) return Either.makeLeft(value.getLeft());
return func(value.getRight()); 2
}
}
正如我们所看到的,实现比 for 的实现更简单:在我们解压值之后,我们简单地返回应用它map()的结果。func()让我们在下一个清单中 使用bind()它来实现我们的功能,并获得所需的无分支错误传播行为。readCatFromFile()
As we can see, the implementation is even simpler than the one for map(): after we unpack the value, we simply return the result of applying func() to it. Let’s use bind() to implement our readCatFromFile() function in the next listing and get the desired branchless error propagation behavior.
函数 readCatFromFile(path: string): Either<Error, Cat> {
让 handle: Either<Error, FileHandle> = openFile(path)
让内容:Either<Error, string> =
Either.bind(handle, readFile); 1个
返回 Either.bind(content, deserializeCat); 2
}function readCatFromFile(path: string): Either<Error, Cat> {
let handle: Either<Error, FileHandle> = openFile(path)
let content: Either<Error, string> =
Either.bind(handle, readFile); 1
return Either.bind(content, deserializeCat); 2
}
此版本将、和 无缝链接在一起,openFile()因此如果任何函数失败,错误将作为. 同样,分支被封装在实现中,所以我们的处理函数是线性的。 readFile()deserialize-Cat()readCatFromFile()bind()
This version seamlessly chains together openFile(), readFile(), and deserialize-Cat() so that if any of the functions fails, the error gets propagated as the result of readCatFromFile(). Again, branching is encapsulated in the bind() implementation, so our processing function is linear.
在继续定义 monad 之前,让我们来看另一个简化的例子并对比map()and bind()。我们将再次使用Box<T>, 一种简单包装 type 值的泛型类型T。尽管这种类型不是特别有用,但它是我们可以拥有的最简单的泛型类型。我们希望专注于如何map()使用bind()类型的值T以及U在某些通用上下文中使用类型的值,例如Box<T>, Box<U>(or T[], U[]; or Optional<T>, Optional<U>; or Either<Error, T>, Either<Error, U>, 等等)。
Before moving on to define monads, let’s take another simplified example and contrast map() and bind(). We’ll again use Box<T>, a generic type that simply wraps a value of type T. Although this type is not particularly useful, it is the simplest generic type we can have. We want to focus on how map() and bind() work with values of types T and U in some generic context, such as Box<T>, Box<U> (or T[], U[]; or Optional<T>, Optional<U>; or Either<Error, T>, Either<Error, U>, and so on).
对于 a Box<T>, functor ( map()) 接受 aBox<T>和一个来自Tto的函数U并返回 a Box<U>。问题是我们有我们的功能直接从T到的场景Box<U>。这是bind()为了什么。从tobind()获取 aBox<T>和一个函数,并返回将函数应用于内部的结果(图11.7)。 TBox<U>TBox<T>
For a Box<T>, a functor (map()) takes a Box<T> and a function from T to U and returns a Box<U>. The problem is that we have scenarios in which our functions are directly from T to Box<U>. This is what bind() is for. bind() takes a Box<T> and a function from T to Box<U> and returns the result of applying the function to the T inside Box<T> (figure 11.7).
如果我们有一个stringify()接受数字并返回其字符串表示形式的函数,我们可以map()在 a 上执行它Box<number>并返回 a Box<string>,如以下清单所示。
If we have a function stringify() that takes a number and returns its string representation, we can map() it on a Box<number> and get back a Box<string>, as shown in the following listing.
命名空间框 {
导出函数 map<T, U>( 1
box: Box<T>, func: (value: T) => U): Box<U> {
返回新的 Box<U>(func(box.value));
}
}
function stringify(value: number): 字符串 { 2
返回值.toString();
}
const s: Box<string> = Box.map(new Box(42), stringify); 3个namespace Box {
export function map<T, U>( 1
box: Box<T>, func: (value: T) => U): Box<U> {
return new Box<U>(func(box.value));
}
}
function stringify(value: number): string { 2
return value.toString();
}
const s: Box<string> = Box.map(new Box(42), stringify); 3
如果不是stringify()从number到string,我们有一个直接从 到 的函数boxify()将不起作用。相反,我们需要,如下一个清单所示…… numberBox<string>map()bind()
If instead of stringify(), which goes from number to string, we have a boxify() function that goes from number directly to Box<string>, map() won’t work. We’ll need bind() instead, as shown in the next listing..
命名空间框 {
导出函数绑定<T, U>(
box: Box<T>, func: (value: T) => Box<U>: Box<U> { 1
返回函数(框。值);
}
}
function boxify(value: number): Box<字符串> { 2
返回新框(value.toString());
}
const b: Box<string> = Box.bind(new Box(42), boxify); 3个namespace Box {
export function bind<T, U>(
box: Box<T>, func: (value: T) => Box<U>): Box<U> { 1
return func(box.value);
}
}
function boxify(value: number): Box<string> { 2
return new Box(value.toString());
}
const b: Box<string> = Box.bind(new Box(42), boxify); 3
map()和的结果bind()仍然是Box<string>. 我们仍然从Box<T>到Box<U>; 不同之处在于我们如何到达那里。在这种map()情况下,我们需要一个函数 from Tto U。在这种bind()情况下,我们需要一个函数 from Tto Box<U>。
The result of both map() and bind() is still a Box<string>. We still go from Box<T> to Box<U>; the difference is how we get there. In the map() case, we need a function from T to U. In the bind() case, we need a function from T to Box<U>.
bind()一个 monad 由一个更简单的函数组成。这个其他函数接受一个类型T并将其包装到泛型类型中,例如Box<T>、T[]、Optional<T>或Either<Error, T>。这个函数通常称为return()or unit()。
A monad consists of bind() and one more, simpler function. This other function takes a type T and wraps it into the generic type, such as Box<T>, T[], Optional<T>, or Either<Error, T>. This function is usually called return() or unit().
monad 允许通用地构建程序,同时封装程序逻辑所需的样板代码。使用 monad,一系列函数调用可以表示为抽象数据管理、控制流或副作用的管道。
A monad allows structuring programs generically while encapsulating away boilerplate code needed by the program logic. With monads, a sequence of function calls can be expressed as a pipeline that abstracts away data management, control flow, or side effects.
让我们看几个 monad 的例子。我们可以从我们的简单Box<T>类型开始,并unit()在下一个清单中添加它来完成 monad。
Let’s look at a few examples of monads. We can start with our simple Box<T> type and add unit() to it in the next listing to complete the monad.
命名空间框 {
导出函数单元<T>(值:T):框<T> { 1
返回新框(值);
}
导出函数绑定<T, U>(
box: Box<T>, func: (value: T) => Box<U>: Box<U> { 2
返回函数(框。值);
}
}namespace Box {
export function unit<T>(value: T): Box<T> { 1
return new Box(value);
}
export function bind<T, U>(
box: Box<T>, func: (value: T) => Box<U>): Box<U> { 2
return func(box.value);
}
}
实现非常简单。让我们看看Optional<T>下面清单中的 monad 函数。
The implementation is very straightforward. Let’s look at the Optional<T> monad functions in the following listing.
命名空间可选{
导出函数单元<T>(值:T):可选<T> { 1
返回新的可选(值);
}
导出函数绑定<T, U>(
可选:可选<T>,
func: (value: T) => 可选<U>: 可选<U> {
如果(!optional.hasValue())返回新的Optional(); 2个
返回函数(可选的。getValue()); 3个
}
}namespace Optional {
export function unit<T>(value: T): Optional<T> { 1
return new Optional(value);
}
export function bind<T, U>(
optional: Optional<T>,
func: (value: T) => Optional<U>): Optional<U> {
if (!optional.hasValue()) return new Optional(); 2
return func(optional.getValue()); 3
}
}
与函子非常相似,如果编程语言不能表达更高种类的类型,我们就没有指定接口的好方法Monad。相反,让我们将 monad 视为一种模式。
Very much as with functors, if a programming language can’t express higher kinded types, we don’t have a good way to specify a Monad interface. Instead, let’s think of monads as a pattern.
monad 是一种泛型类型H<T>,我们有一个类似的函数unit()接受一个类型的值T并返回一个类型的值H<T>,而一个类似的函数bind()接受一个类型的值H<T>和一个来自Tto的函数H<U>,并返回一个类型的值H<U>。
A monad is a generic type H<T> for which we have a function like unit() that takes a value of type T and returns a value of type H<T>, and a function like bind() that takes a value of type H<T> and a function from T to H<U>, and returns a value of type H<U>.
请记住,由于大多数语言都使用这种模式,无法指定接口供编译器检查,因此在许多情况下,这两个函数和unit()可能bind()会以不同的名称出现。您可能会听到术语monadic,如monadic error handling,这意味着错误处理遵循 monad 模式。
Bear in mind that because most languages use this pattern, without a way to specify an interface for the compiler to check, in many instances the two functions, unit() and bind(), may show up under different names. You may hear the term monadic, as in monadic error handling, which means that error handling follows the monad pattern.
接下来,我们将看另一个例子。你可能会惊讶地发现这个例子出现在本书前面的 第 6 章中;我们只是还没有一个名字。
Next, we’ll look at another example. You may be surprised to see that this example showed up much earlier in this book, in chapter 6; we just didn’t have a name for it yet.
在第 6 章中,我们研究了简化异步代码的方法。我们最终看到了承诺。承诺表示将在未来某个时间发生的计算结果。Promise<T>是类型值的承诺T。我们可以使用函数通过链接承诺来安排异步代码的执行then()。
In chapter 6, we looked at ways to simplify asynchronous code. We ended up looking at promises. A promise represents the result of a computation that will happen sometime in the future. Promise<T> is the promise of a value of type T. We can schedule execution of asynchronous code by chaining promises, using the then() function.
假设我们有一个函数可以确定我们在地图上的位置。因为这个函数会与 GPS 一起工作,可能需要更长的时间才能完成,所以我们让它异步。它将返回类型的承诺Promise<Location>。接下来,我们有一个函数,在给定位置的情况下,它会联系拼车服务来为我们提供一个Car,如下一个清单所示。
Let’s say we have a function that determines our location on the map. Because this function will work with the GPS, it may take longer to finish, so we make it asynchronous. It will return a promise of type Promise<Location>. Next, we have a function that, given a location, will contact a ride-sharing service to get us a Car, as the next listing shows.
声明函数 getLocation(): Promise<Location>; 声明函数 hailRideshare(location: Location): Promise<Car>; 让汽车:Promise<Car> = getLocation().then(hailRideshare); 1个
declare function getLocation(): Promise<Location>; declare function hailRideshare(location: Location): Promise<Car>; let car: Promise<Car> = getLocation().then(hailRideshare); 1
在这一点上,这对您来说应该非常熟悉。then()究竟是怎样的Promise<T>法术bind()!
This should look very familiar to you at this point. then() is just how Promise<T> spells bind()!
正如我们在第 6 章中看到的,我们还可以通过使用创建一个立即解决的承诺Promise.resolve()。这需要一个值并返回一个包含该值的已解决的承诺,这Promise<T>相当于unit().
As we saw in chapter 6, we can also create an instantly resolved promise by using Promise.resolve(). This takes a value and returns a resolved promise containing that value, which is the Promise<T> equivalent of unit().
事实证明,几乎所有主流编程语言都可用的 API 链式承诺是单子的。它遵循我们在本节中看到的相同模式,但在不同的域中。在处理错误传播时,我们的 monad 封装了检查我们是否有一个可以继续操作的值或是否有一个我们应该传播的错误。通过承诺,monad 封装了调度和恢复执行的复杂性。不过模式是一样的。
It turns out that chaining promises, an API available in virtually all mainstream programming languages, is monadic. It follows the same pattern that we saw in this section, but in a different domain. While dealing with error propagation, our monad encapsulated checking whether we have a value that we can continue operating on or have an error that we should propagate. With promises, the monad encapsulates the intricacies of scheduling and resuming execution. The pattern is the same, though.
另一个常用的 monad 是列表 monad。让我们看一下基于序列的实现:一个divisors()函数,它接受一个数字n并返回一个包含除 1 和它本身之外的所有除数的数组n,如清单 11.32所示。
Another commonly used monad is the list monad. Let’s look at an implementation over sequences: a divisors() function that takes a number n and returns an array containing all of its divisors except 1 and n itself, as shown in listing 11.32.
这个简单的实现从 2 开始,一直到 n 的一半,然后将它找到的所有除以n无余数的数字相加。有更有效的方法来找到一个数的所有约数,但在这种情况下我们将坚持使用一个简单的算法。
This straightforward implementation starts from 2 and goes up to half of n, and adds all numbers it finds that divide n without a remainder. There are more efficient ways to find all divisors of a number, but we’ll stick to a simple algorithm in this case.
函数除数(n:数字):数字[] {
让结果:数字[] = [];
对于(设 i = 2;i <= n / 2;i++){
如果(n%我==0){
结果.推(我);
}
}
返回结果;
}function divisors(n: number): number[] {
let result: number[] = [];
for (let i = 2; i <= n / 2; i++) {
if (n % i == 0) {
result.push(i);
}
}
return result;
}
现在假设我们想要获取一个数字数组并返回一个包含它们所有除数的数组。我们不需要担心受骗。实现此目的的一种方法是提供一个函数,该函数采用一组输入数字,应用于divisors()每个输入,并将所有调用的结果连接divisors()到最终结果中,如以下代码所示。
Now let’s say we want to take an array of numbers and return an array containing all their divisors. We don’t need to worry about dupes. One way to do this is to provide a function that takes an array of input numbers, applies divisors() to each of them, and joins the results of all the calls to divisors() into a final result, as shown in the following code.
function allDivisors(ns: number[]): number[] {
让结果:数字[] = [];
for (const n of ns) {
结果 = result.concat(除数(n));
}
返回结果;
}function allDivisors(ns: number[]): number[] {
let result: number[] = [];
for (const n of ns) {
result = result.concat(divisors(n));
}
return result;
}
事实证明,这种模式很常见。假设我们有另一个函数 ,anagrams()它生成一个字符串的所有排列并返回一个字符串数组。如果我们想要获得一个字符串数组的所有变位词的集合,我们最终会实现一个非常相似的函数,如下一个清单所示。
It turns out that this pattern is common. Let’s say that we have another function, anagrams(), that generates all permutations of a string and returns an array of strings. If we want to get the set of all anagrams of an array of strings, we would end up implementing a very similar function, as the next listing shows.
声明函数 anagram(input: string): string[]; 1个
函数 allAnagrams(输入:字符串[]):字符串[] { 2
让结果:string[] = [];
对于(输入的常量输入){
结果 = result.concat(字谜(输入));
}
返回结果;
}declare function anagram(input: string): string[]; 1
function allAnagrams(inputs: string[]): string[] { 2
let result: string[] = [];
for (const input of inputs) {
result = result.concat(anagram(input));
}
return result;
}
现在让我们看看我们是否可以在下一个清单中用通用函数替换allDivisors()and 。allAnagrams()此函数将接受一个 s 数组T和一个函数 fromT到一个 s 数组,并返回一个s U数组。U
Now let’s see whether we can replace allDivisors() and allAnagrams() with a generic function in the next listing. This function would take an array of Ts and a function from T to an array of Us, and return an array of Us.
function bind<T, U>(inputs: T[], func: (value: T) => U[]): U[] { 1
让结果:U[] = [];
对于(输入的常量输入){
结果 = result.concat(函数(输入)); 2个
}
返回结果;
}
function allDivisors(ns: number[]): number[] { 3
返回绑定(ns,除数);
}
函数 allAnagrams(输入:字符串[]):字符串[] { 4
返回绑定(输入,字谜);
}function bind<T, U>(inputs: T[], func: (value: T) => U[]): U[] { 1
let result: U[] = [];
for (const input of inputs) {
result = result.concat(func(input)); 2
}
return result;
}
function allDivisors(ns: number[]): number[] { 3
return bind(ns, divisors);
}
function allAnagrams(inputs: string[]): string[] { 4
return bind(inputs, anagram);
}
您可能已经猜到了,这是bind()列表 monad 的实现。对于列表,bind()将每次调用给定函数返回的数组展平为单个数组。错误传播 monad 决定是传播错误还是应用函数,continuation monad 包装调度,而列表 monad 将一组结果(列表的列表)组合到一个平面列表中。在这种情况下,框是一系列值(图 11.8)。
As you’ve probably guessed, this is the bind() implementation for the list monad. In the case of lists, bind() flattens the arrays returned by each call of the given function into a single array. While the error-propagating monad decides whether to propagate an error or apply a function and the continuation monad wraps scheduling, the list monad combines a set of results (a list of lists) into a single flat list. In this case, the box is a sequence of values (figure 11.8).
实现unit()是微不足道的。给定 type 的值T,它返回一个仅包含该值的列表。这个 monad 泛化到所有类型的列表:数组、链表和迭代器范围。
The unit() implementation is trivial. Given a value of type T, it returns a list containing just that value. This monad generalizes to all kinds of lists: arrays, linked lists, and iterator ranges.
函子和单子来自范畴论,这是数学的一个分支,处理由对象和这些对象之间的箭头组成的结构。使用这些小构建块,我们可以构建诸如仿函数和单子之类的结构。我们现在不谈它的细节;我们只是说多个领域,比如集合论甚至类型系统,都可以用范畴论来表达。
Functors and monads come from category theory, a branch of mathematics that deals with structures consisting of objects and arrows between these objects. With these small building blocks, we can build up structures such as functors and monads. We won’t go into its details now; we’ll just say that multiple domains, like set theory and even type systems, can be expressed in category theory.
Haskell 是一种从范畴论中汲取了很多灵感的编程语言,因此它的语法和标准库使得表达函子、单子和其他结构等概念变得容易。Haskell 完全支持更高种类的类型。
Haskell is a programming language that took a lot of inspiration from category theory, so its syntax and standard library make it easy to express concepts such as functors, monads, and other structures. Haskell fully supports higher kinded types.
也许是因为范畴论的构建块非常简单,我们一直在讨论的抽象概念适用于如此多的领域。我们刚刚看到 monad 在错误传播、异步代码和序列处理的上下文中很有用。
Perhaps because the building blocks of category theory are so simple, the abstractions we’ve been talking about are applicable across so many domains. We just saw that monads are useful in the context of error propagation, asynchronous code, and sequence processing.
尽管大多数主流语言仍然将 monad 视为模式而不是适当的构造,但它们绝对是在不同上下文中反复出现的有用结构。
Although most mainstream languages still treat monads as patterns instead of proper constructs, they are definitely useful structures that show up over and over in different contexts.
state monad 和 IO monad 是其他几个常见的 monad,它们在具有纯函数(没有副作用的函数)和不可变数据的函数式编程语言中很流行。我们将只提供这些 monad 的高级概述,但如果您决定学习一种函数式编程语言,例如 Haskell,您很可能会在旅程的早期遇到它们。
A couple of other common monads, which are popular in functional programming languages with pure functions (functions that don’t have side effects) and immutable data, are the state monad and the IO monad. We’ll provide only a high-level overview of these monads, but if you decide to learn a functional programming language such as Haskell, you will likely encounter them early in your journey.
状态 monad 封装了一段状态,它随值一起传递。这个 monad 使我们能够编写纯函数,在给定当前状态的情况下,生成一个值和一个更新的状态。将这些链接在一起bind()允许我们通过管道传播和更新状态,而无需将其显式存储在变量中,从而使纯功能代码能够处理和更新状态。
The state monad encapsulates a piece of state that it passes along with a value. This monad enables us to write pure functions that, given a current state, produce a value and an updated state. Chaining these together with bind() allows us to propagate and update state through a pipeline without explicitly storing it in a variable, enabling purely functional code to process and update state.
IO monad 封装了副作用。它允许我们实现仍然可以读取用户输入或写入文件或终端的纯函数,因为不纯的行为已从函数中移除并包装在 IO monad 中。
The IO monad encapsulates side effects. It allows us to implement pure functions that can still read user input or write to a file or terminal because the impure behavior is removed from the function and wrapped in the IO monad.
如果您有兴趣了解更多信息,第 11.3 节提供了一些供进一步研究的资源。
If you are interested in learning more, section 11.3 provides some resources for further study.
让我们采用Lazy<T>定义为 的函数类型() => T,一个不带参数并返回类型值的函数T。这是Lazy因为它会产生一个T,但只有在我们要求时才会产生。为这种类型 实施unit()、map()和。bind()
Let’s take the function type Lazy<T> defined as () => T, a function that takes no arguments and returns a value of type T. It’s Lazy because it produces a T, but only when we ask it to. Implement unit(), map(), and bind() for this type.
我们已经涵盖了很多基础知识,从基本类型和组合,到函数类型、子类型、泛型,以及一小部分更高级的类型。不过,我们只是触及了类型系统世界的皮毛。在这最后一节中,我们将探讨一些您可能有兴趣了解更多的主题,并为每个主题提供一些起点。
We have covered a lot of ground, from primitive types and composition, to function types, subtyping, generics, and a sliver of higher kinded types. Still, we’ve barely scratched the surface of the world of type systems. In this final section, we’ll look at a few topics you may be interested in learning more about and provide some starting points for each one.
函数式编程是一种与面向对象编程截然不同的范例。学习函数式编程语言为您提供了另一种思考代码的方式。解决问题的方法越多,分解和解决问题就越容易。
Functional programming is a very different paradigm from object-oriented programming. Learning a functional programming language gives you another way to think about code. The more ways you have to approach a problem, the easier it is to break it down and solve it.
越来越多来自函数式编程的特性和模式正在进入非函数式语言,这证明了它们的适用性。Lambda 和闭包、不可变数据结构和反应式编程都来自函数世界。
More and more features and patterns from functional programming are making their way into nonfunctional languages, which is a testament to their applicability. Lambdas and closures, immutable data structures, and reactive programming all come from the functional world.
最好的入门方法是选择一种函数式编程语言。我推荐 Haskell 作为入门语言。它具有相当简单的语法和非常强大的类型系统,并且建立在坚实的理论基础之上。一本很好的、易于阅读的关于该主题的介绍性书籍是Learn You a Haskell for Great Good!Miran Lipovaca 着,No Starch 出版社出版。
The best way to get started is to pick up a functional programming language. I recommend Haskell as a starting language. It has a fairly simple syntax and a very powerful type system, and it stands on a solid theoretical foundation. A good, easy-to-read introductory book on the topic is Learn You a Haskell for Great Good! by Miran Lipovaca, published by No Starch Press.
正如我们在前几章中看到的,泛型编程支持极其强大的抽象和代码可重用性。泛型编程随着 C++ 标准模板库及其数据结构和算法的混合匹配集合而流行起来。
As we saw in previous chapters, generic programming enables extremely powerful abstractions and code reusability. Generic programming became popular with the C++ standard template library and its mix-and-match collection of data structures and algorithms.
泛型编程起源于抽象代数。Alexander Stepanov 创造了泛型编程这个术语并实现了原始模板库,他写了两本关于这个主题的书:Elements of Programming(与 Paul McJones 合着)和From Mathematics to Generic Programming(与 Daniel E. Rose 合着),均由Addison-Wesley Professional。
Generic programming has its roots in abstract algebra. Alexander Stepanov, who coined the term generic programming and implemented the original template library, wrote two books on the subject: Elements of Programming (coauthored with Paul McJones) and From Mathematics to Generic Programming (coauthored with Daniel E. Rose), both published by Addison-Wesley Professional.
这两本书都涉及到一些数学知识,但我希望这个事实不会让您气馁。代码的优雅和美丽令人惊叹。潜在的主题是,通过正确的抽象,我们不需要妥协:我们可以拥有简洁、高效、易于阅读和优雅的代码。
Both books leverage some math, but I hope that fact won’t discourage you. The elegance and beauty of the code are astonishing. The underlying theme is that with the right abstractions, we don’t need to compromise: we can have code that is succinct, performant, easy to read, and elegant.
正如我们前面提到的,函子等结构直接来自范畴论。Bartosz Milewski 的Category Theory for Programmers(自行出版)是对该领域的一个非常易于阅读的介绍。
As we mentioned earlier, constructs such as functors come directly from category theory. Bartosz Milewski’s Category Theory for Programmers (self-published) is a surprisingly easy-to-read introduction to this field.
我们讨论了函子和 monad,但对于更高种类的类型还有很多。可能需要一段时间才能渗透到更主流的语言中,但是如果您想领先一步,Haskell 是一种很好的语言,可以用来掌握这些概念。
We talked about functors and monads, but there is a lot more to higher kinded types. It will probably take a while for things to trickle down to more mainstream languages, but if you want to get ahead of the curve, Haskell is a good language with which to grasp these concepts.
能够指定更高级别的抽象(例如 monad)使我们能够编写更可重用的代码。
Having the ability to specify higher-level abstractions such as monads enables us to write even-more-reusable code.
本书没有足够的篇幅来介绍依赖类型,但如果您想了解强大的类型系统使代码更安全的更多方式,这个主题是另一个不错的话题。
We didn’t have space to cover dependent types in this book, but if you want to know more ways that a powerful type system makes code safer, this topic is another good one.
非常简单地,我们看到了一个类型如何决定一个变量可以取的值。我们还研究了泛型,因为一个类型可以指示另一个类型可以是什么(类型参数)。依赖类型扭转了这种情况:我们有决定类型的值。典型的例子是在类型系统中对列表的长度进行编码。例如,包含两个元素的数字列表最终与包含五个元素的数字列表具有不同的类型。将它们连接起来给了我们另一种类型:一个有七个元素的列表。您可以想象在类型系统中对此类信息进行编码如何保证,例如,我们永远不会索引越界。
Very briefly, we saw how a type can dictate the values that a variable can take. We also looked at generics, in that a type can dictate what another type can be (type parameters). Dependent types flip this situation around: we have values that dictate types. The classic example is encoding the length of a list in the type system. A list of numbers with two elements ends up having a different type from a list of numbers with five elements, for example. Concatenating them gives us another type: a list with seven elements. You can imagine how encoding such information in the type system can guarantee, for example, that we never index out of bounds.
如果您想了解有关依赖类型的更多信息,我推荐Edwin Brady 的Type Driven Development with Idris,Manning 出版。Idris 是一种语法与 Haskell 非常相似的编程语言,但它增加了对依赖类型的支持。
If you want to learn more about dependent types, I recommend Type Driven Development with Idris by Edwin Brady, published by Manning. Idris is a programming language with a syntax very similar to Haskell’s, but it adds support for dependent types.
在第 1 章中,我们简要提到了类型系统和逻辑之间的深层联系。线性逻辑与处理资源的经典逻辑不同。与经典逻辑不同,在经典逻辑中,演绎如果为真,则永远为真,而线性逻辑证明会消耗演绎。
In chapter 1, we briefly mentioned the deep connection between type systems and logic. Linear logic is a different take on classic logic that deals with resources. Unlike classic logic, in which a deduction, if true, is true forever, a linear logic proof consumes deductions.
这在编程语言中有直接应用,其中在类型系统中使用线性类型编码资源使用跟踪。Rust 是一种正在稳步流行的编程语言;它使用线性类型来确保资源安全。它的借用检查器确保资源始终只有一个所有者。如果我们将对象传递给函数,我们就转移了资源的所有权,并且编译器不再允许我们引用资源,直到函数交回资源。这种情况旨在消除并发问题,以及 C 可怕的“释放后使用”和“双重释放”。
This has a direct application in programming languages, in which using linear types in a type system encodes resource use tracking. Rust is a programming language that is steadily gaining in popularity; it uses linear types to ensure resource safety. Its borrow checker ensures that there is always a single owner of a resource. If we pass an object to a function, we transfer ownership of the resource, and the compiler no longer allows us to reference the resource until the function hands back the resource. This situation aims to eliminate concurrency issues, as well as the dreaded “use after free” and “double free” of C.
Rust 是另一种值得学习的好语言,因为它具有强大的通用支持和独特的安全特性。Rust 编程语言书籍可在 Rust 网站上免费获得,并很好地介绍了该语言 ( https://doc.rust-lang.org/book )。
Rust is another good language to learn for its powerful generic support and unique safety features. The Rust Programming Language book is available for free on the Rust website and provides a good introduction to the language (https://doc.rust-lang.org/book).
一个可能的实现使用我们在第 5 章中重述的面向对象的装饰器模式来提供另一种类型实现IReader<U>,它包装了一个IReader<T>并且在read()被调用时将给定函数映射到原始值上:
接口 IReader<T> { 读取():T; } 命名空间 IReader { 类 MappedReader<T, U> 实现 IReader<U> { 阅读器:IReader<T>; 函数:(值:T)=> U; constructor(reader: IReader<T>, func: (value: T) => U) { this.reader = 读者; 这个.func = func; } 读():你{ 返回 this.func(this.reader.read()); } } 导出函数 map<T, U>(reader: IReader<T>, func: (value: T) => U) : IReader<U> { 返回新的 MappedReader(reader, func); } }
A possible implementation uses the object-oriented decorator pattern we recapped in chapter 5 to provide another type implementing IReader<U> that wraps an IReader<T> and, when read() is called, maps the given function over the original value:
interface IReader<T> { read(): T; } namespace IReader { class MappedReader<T, U> implements IReader<U> { reader: IReader<T>; func: (value: T) => U; constructor(reader: IReader<T>, func: (value: T) => U) { this.reader = reader; this.func = func; } read(): U { return this.func(this.reader.read()); } } export function map<T, U>(reader: IReader<T>, func: (value: T) => U) : IReader<U> { return new MappedReader(reader, func); } }
一个可能的实现如下。map()注意和之间的区别bind()。
输入 Lazy<T> = () => T; 命名空间懒惰{ 导出函数单元<T>(值:T):惰性<T> { 返回()=>值; } 导出函数 map<T, U>(lazy: Lazy<T>, func: (value: T) => U) : 懒惰<U> { 返回()=>函数(懒惰()); } 导出函数 bind<T, U>(lazy: Lazy<T>, func: (value: T) => 懒惰<U>) : 懒惰<U> { 返回函数(惰性()); } }
A possible implementation follows. Notice the difference between map() and bind().
type Lazy<T> = () => T; namespace Lazy { export function unit<T>(value: T): Lazy<T> { return () => value; } export function map<T, U>(lazy: Lazy<T>, func: (value: T) => U) : Lazy<U> { return () => func(lazy()); } export function bind<T, U>(lazy: Lazy<T>, func: (value: T) => Lazy<U>) : Lazy<U> { return func(lazy()); } }
对于简单的代码,例如尝试一些没有依赖项的代码示例,您可以使用 https://www.typescriptlang.org/play上的在线 TypeScript playground 。
For simple code, such as trying out some code samples without dependencies, you can use the online TypeScript playground at https://www.typescriptlang.org/play.
要在本地安装,您首先需要 Node.js 和 npm,即 Node 包管理器。您可以在https://www.npmjs.com/get-npm获取它们。有了这些后,运行npm install -g typescript以安装 TypeScript 编译器。
To install locally, you first need Node.js and npm, the Node Package Manager. You can get them at https://www.npmjs.com/get-npm. When you have those, run npm install -g typescript to install the TypeScript compiler.
您可以通过将单个 TypeScript 文件作为参数传递给 TypeScript 编译器来编译它,例如tsc helloworld.ts. TypeScript 编译成 JavaScript。
You can compile a single TypeScript file by passing it as an argument to the TypeScript compiler, such as tsc helloworld.ts. TypeScript compiles to JavaScript.
对于包含多个文件的项目,tsconfig.json 文件用于配置编译器。从带有 tsconfig.json 文件的目录中不带参数运行tsc将根据配置编译整个项目。
For projects that contain multiple files, a tsconfig.json file is used to configure the compiler. Running tsc with no arguments from a directory with a tsconfig.json file will compile the whole project according to the configuration.
本书中的代码示例可从https://github.com/vladris/programming-with-types获得。每章都在自己单独的目录中,并有自己的 tsconfig.json。
The code samples in this book are available at https://github.com/vladris/programming-with-types. Each chapter is in its own separate directory and has its own tsconfig.json.
代码是使用 TypeScript 3.3 版构建的,目标是 ES6 标准,带有strict设置。
Code was built with version 3.3 of TypeScript, targeting the ES6 standard, with strict settings.
每个示例文件都是独立的,因此运行代码示例所需的所有类型和函数都内联在每个示例文件中。每个示例文件都使用唯一的命名空间来防止命名冲突,因为一些示例提供了相同功能或模式的不同实现。
Each sample file is stand-alone, so all types and functions required to run a code sample are inlined within each sample file. Each sample file uses a unique namespace to prevent naming conflicts, because some examples present different implementations of the same function or pattern.
要运行示例文件,请使用tsc;进行编译 然后用 Node.js 运行编译后的 JavaScript 文件。tsc helloworld.ts例如用 编译后,用 运行node helloworld.js。
To run a sample file, compile by using tsc; then run the compiled JavaScript file with Node. After compiling with tsc helloworld.ts, for example, run with node helloworld.js.
这本书涵盖了 TypeScript 中变体和其他类型的 DIY 实现。对于这些类型的 C# 和 Java 版本,请查看 Maki 类型库:https://github.com/vladris/maki。
The book covers DIY implementations for variant and other types in TypeScript. For C# and Java versions of these types, check out the Maki type library: https://github.com/vladris/maki.
这个备忘单并不详尽。它涵盖了本书中使用的 TypeScript 语法子集。有关完整的 TypeScript 参考,请参阅http://www.typescriptlang.org/docs。
This cheat sheet is not exhaustive. It covers the TypeScript syntax subset used in this book. For a full TypeScript reference, see http://www.typescriptlang.org/docs.
|
类型 Type |
描述 Description |
|---|---|
| 布尔值 | 可以是真也可以是假。 |
| 数字 | 64 位浮点数。 |
| 细绳 | UTF-16 Unicode 字符串。 |
| 空白 | 用作不返回有意义值的函数的返回类型的类型。 |
| 不明确的 | 只能是未定义的。例如,表示已声明但未初始化的变量。 |
| 无效的 | 只能为空。 |
| 目的 | 表示对象或非原始类型。 |
| 未知 | 可以表示任何值。类型安全,因此它不会隐式转换为另一种类型。 |
| 任何 | 绕过类型检查。类型不安全并自动转换为任何其他类型。 |
| 绝不 | 不能代表任何价值 |
|
描述 Description |
|
|---|---|
| 细绳[] | 数组类型在类型名称后用 [] 表示——在本例中,是一个字符串数组。 |
| [数字,字符串] | 元组被声明为 [] 中的类型列表——在本例中,是一个数字和一个字符串,例如 [0, "hello"]。 |
| (x: 数字, y: 数字) => 数字; | 函数类型声明为 () 中的参数列表,然后是 =>,然后是返回类型。 |
|
枚举方向 { 北, 东, 南, 西, } |
枚举是用关键字 enum 声明的。在这种情况下,值可以是文字 North、East、South 或 West 之一。 |
|
输入点 { X:数字, Y:数字 } |
具有类型数字的 X 和 Y 属性的类型。 |
|
接口 IExpression { 评估():数字; } |
带有返回数字的 evaluate() 方法的接口。 |
|
类 Circle extends Shape implements IGeometry { // ... } |
Circle 类扩展了 Shape 基类并实现了 IGeometry 接口。 |
| 类型 Shape = Circle | 正方形; | 联合类型声明为 | 分隔的类型列表。形状是圆形或方形。 |
|
类型 SerializableExpression = Serializable & Expression; |
交集类型被声明为一个 & 分隔的类型列表。SerializableExpression 具有 Serializable 的所有成员和 Expression 的所有成员。 |
|
例子 Example |
描述 Description |
|---|---|
|
让 x: 未知 = 0; 让 y: number = <number>x; 输入 Point = { x: 数字; y:数字; } function isPoint(p: unknown): p is Point { return ((<Point>p).x !== undefined) && ((<Point>p).y !== undefined); } 让 p: 未知 = { x: 10, y: 10 }; if (isPoint(p)) { // p 是点类型 px -= 10; } |
在值之前的 <> 之间指定类型会将值重新解释为该类型。只有在明确地重新解释为数字后,才能将 x 分配给 y。类型谓词是一个布尔值,表示变量属于特定类型。如果我们将 p 重新解释为一个点,并且它同时具有 x 和 ay 成员(均未定义),则 p 是一个点。在类型谓词为真的 if 语句中,测试值会自动重新解释为具有该类型。 |
|
名称[部分] Name [Section] |
类型稿类型 TypeScript type |
可能的值 Possible values |
|---|---|---|
| 空类型 [2.1.1] | 绝不 | 没有可能的值 |
| 单元类型 [2.1.2] | 空白 | 一个可能的值 |
| 求和类型 [3.4.2] | 编号 | 细绳 | 来自数字的值或来自字符串的值 |
| 元组(产品类型)[3.1.1] | [数字,字符串] | 来自数字的值和来自字符串的值 |
| 记录(产品类型)[3.1.2] | { 一个号码; b:字符串;} | 来自数字的(命名)值和来自字符串的(命名)值 |
| 函数类型[5.1.2] | (值:数字)=> 字符串 | 函数编号 → 字符串 |
| 顶部类型[7.2.1] | 未知 | 任何类型的值 |
| 底部类型[7.2.2] | 绝不 | 没有可能的值(底部类型是任何其他类型的子类型) |
| 界面[8.1] | 接口 ILogger { /* ... */ } | 实现 ILogger 接口的类型的对象 |
| 班级[8.2.1] | 广场类 { /* ... */ } | 方形对象 |
| 交叉路口类型[8.4.3] | 方形和可记录 | 具有 Square 和 Loggable 成员的对象 |
| 通用类 [9.2.1] | 类列表<T> { /* ... */ } | 具有类型参数 T 的泛型类 List |
| 通用函数 [9.1.1] | 输入 Func<T, U> = (arg: T) => U; | 来自 T → U 的函数,其中 T 和 U 是类型参数 |
map()将函数应用于范围的每个值并返回应用该函数的结果。
map() applies a function to each value of a range and returns the results of applying that function.
地图([“苹果”,“橙子”,“桃子”],(项目)=> item.length)
map(["apple", "orange", "peach"], (item) => item.length)
filter()将谓词应用于范围的每个值并过滤掉谓词为假的值。
filter() applies a predicate to each value of a range and filters out the values for which the predicate is false.
filter(["苹果", "橙子", "桃子"], (item) => item.length == 5)
filter(["apple", "orange", "peach"], (item) => item.length == 5)
reduce()使用给定函数组合范围内的值并返回单个值。
reduce() combines the values of a range using a given function and returns a single value.
reduce(["苹果", "橙子", "桃子"], "", (acc, item) => acc + item)
reduce(["apple", "orange", "peach"], "", (acc, item) => acc + item)
[符号][ A ][ B ][ C ][ D ][ E ][ F ][ G ][ H ][ I ][ J ][ K ][ L ][ M ][ N ][ O ][ P ][ Q ][ R ][ S ][ T ][ U ][ V ][ W ][ Y ][ Z ]
[SYMBOL][A][B][C][D][E][F][G][H][I][J][K][L][M][N][O][P][Q][R][S][T][U][V][W][Y][Z]
! character
&& characters
| type operator, 2nd, 3rd
|| characters
32-bit integer
4-bit signed integer encoding
4-bit unsigned integer
8-bit unsigned integer
! character
&& characters
| type operator, 2nd, 3rd
|| characters
32-bit integer
4-bit signed integer encoding
4-bit unsigned integer
8-bit unsigned integer
abstract class
accumulate() function
actions
adapter pattern
adaptive algorithms
ADTs (algebraic data types), 2nd, 3rd
product types
sum types
Aggregate() function
aggregate() function
algebraic data types.
See ADTs (algebraic data types).
all() function, 2nd
AND operator
anonymous function
anonymous functions
anonymous functions (lambdas)
any keyword
Any type
any type, 2nd, 3rd, 4th
any() function, 2nd
arithmetic overflow
arrays, 2nd, 3rd
associative arrays
binary trees
fixed-size arrays
implementation trade-offs
list efficiency
associative arrays
associativity
asterisks
async/await function
asynchronous code, simplifying
async/await function
promises
chaining
chaining synchronous functions
creating, 2nd
handling errors
asynchronous execution
callbacks
models for
abstract class
accumulate() function
actions
adapter pattern
adaptive algorithms
ADTs (algebraic data types), 2nd, 3rd
product types
sum types
Aggregate() function
aggregate() function
algebraic data types.
See ADTs (algebraic data types).
all() function, 2nd
AND operator
anonymous function
anonymous functions
anonymous functions (lambdas)
any keyword
Any type
any type, 2nd, 3rd, 4th
any() function, 2nd
arithmetic overflow
arrays, 2nd, 3rd
associative arrays
binary trees
fixed-size arrays
implementation trade-offs
list efficiency
associative arrays
associativity
asterisks
async/await function
asynchronous code, simplifying
async/await function
promises
chaining
chaining synchronous functions
creating, 2nd
handling errors
asynchronous execution
callbacks
models for
bad state
begin iterator, 2nd, 3rd, 4th, 5th
biased exponent
bidirectional iterators, 2nd
Big O notation
BigInt type
binary trees
binary64 encoding
bind() function, 2nd
bit widths
bivariance
bivariant types
Boolean expressions
Boolean types, 2nd, 3rd, 4th, 5th
Boolean expressions
short circuit evaluation
bottom type
bad state
begin iterator, 2nd, 3rd, 4th, 5th
biased exponent
bidirectional iterators, 2nd
Big O notation
BigInt type
binary trees
binary64 encoding
bind() function, 2nd
bit widths
bivariance
bivariant types
Boolean expressions
Boolean types, 2nd, 3rd, 4th, 5th
Boolean expressions
short circuit evaluation
bottom type
callbacks
catch() function, 2nd
category theory, 2nd
clone() function
closed type
closures
code points
coding against interfaces
coercion
collections, subtyping and
composability
composition, 2nd, 3rd
algebraic data types
product types
sum types
composite classes
compound types
assigning meaning
maintaining invariants
tuples
either-or types
enumerations
optional types
result or error
variants
extending behavior with
has-a rule of thumb
implementing
visitor design pattern
alternative implementation of
naïve implementation of
variant visitor function
compound types, 2nd
assigning meaning
maintaining invariants
tuples
conditional branching
const notation
constant space (O(1))
constant time (O(1))
constness property
constraints
enforcing
with constructor
with factory
type parameter constraints
generic algorithms with
generic data structures with
continuation monad
continuations
contracts
contracts (interfaces)
contravariant type, 2nd
correctness
counters
functional counters
implementing
object-oriented counters
resumable counters
covariant types, 2nd
cross-cutting concerns
currency addition function
Curry-Howard correspondence
callbacks
catch() function, 2nd
category theory, 2nd
clone() function
closed type
closures
code points
coding against interfaces
coercion
collections, subtyping and
composability
composition, 2nd, 3rd
algebraic data types
product types
sum types
composite classes
compound types
assigning meaning
maintaining invariants
tuples
either-or types
enumerations
optional types
result or error
variants
extending behavior with
has-a rule of thumb
implementing
visitor design pattern
alternative implementation of
naïve implementation of
variant visitor function
compound types, 2nd
assigning meaning
maintaining invariants
tuples
conditional branching
const notation
constant space (O(1))
constant time (O(1))
constness property
constraints
enforcing
with constructor
with factory
type parameter constraints
generic algorithms with
generic data structures with
continuation monad
continuations
contracts
contracts (interfaces)
contravariant type, 2nd
correctness
counters
functional counters
implementing
object-oriented counters
resumable counters
covariant types, 2nd
cross-cutting concerns
currency addition function
Curry-Howard correspondence
data structures, defined
declarations
decorator pattern
closures
functional decorator
implementing
decoupling independent concerns
generic types
optional types
reusable identity functions
dependent types
deserialization
design patterns
adapter pattern
decorator pattern
strategy pattern
visitor pattern
diamond inheritance problem
dictionaries
downcasts
drop() function
dynamic typing
data structures, defined
declarations
decorator pattern
closures
functional decorator
implementing
decoupling independent concerns
generic types
optional types
reusable identity functions
dependent types
deserialization
design patterns
adapter pattern
decorator pattern
strategy pattern
visitor pattern
diamond inheritance problem
dictionaries
downcasts
drop() function
dynamic typing
eager evaluation, 2nd
Either type, 2nd, 3rd, 4th, 5th, 6th
either value or error
either-or types, 2nd
enumerations
optional types
result or error
exceptions
variants
empty types, 2nd
encapsulation, 2nd
encoding libraries
encodings
UTF-16
UTF-32
UTF-8
end iterator, 2nd, 3rd, 4th, 5th
enum keyword, 2nd
error cases
higher kinded types
promises
values for
error codes
errors, 2nd, 3rd, 4th
event loops
explicit type cast, 2nd
exponents
extend() method
extending behavior
with composition
with mix-ins
extends keyword
eager evaluation, 2nd
Either type, 2nd, 3rd, 4th, 5th, 6th
either value or error
either-or types, 2nd
enumerations
optional types
result or error
exceptions
variants
empty types, 2nd
encapsulation, 2nd
encoding libraries
encodings
UTF-16
UTF-32
UTF-8
end iterator, 2nd, 3rd, 4th, 5th
enum keyword, 2nd
error cases
higher kinded types
promises
values for
error codes
errors, 2nd, 3rd, 4th
event loops
explicit type cast, 2nd
exponents
extend() method
extending behavior
with composition
with mix-ins
extends keyword
filter function
filter/reduce pipeline
generic versions of
filter() function, 2nd, 3rd, 4th, 5th, 6th
final keyword
find() function, 2nd, 3rd
first-class functions
first-order function
first() function
fixed-size arrays, 2nd, 3rd
floating-point numbers
floating-point types
comparing floating-point numbers
precision values
fmap() function
fold() function, 2nd
forward iterators
function argument types, subtyping and
function keyword
function map
function return types, subtyping and
function types, 2nd
adapter pattern
counters
functional counters
implementing
object-oriented counters
resumable counters
decorator pattern
closures
functional decorator
implementing
functional programming
higher-order functions
filter
library support
map
reduce
lazy values
lambdas
long-running operations
asynchronous execution
synchronous execution
simplifying asynchronous code
async/await
promises
state machines without switch statements
early programming with types
implementing
overview
strategy pattern
first-class functions
implementing
typing functions
functional counters
functional programming, 2nd, 3rd
functors
filter function
filter/reduce pipeline
generic versions of
filter() function, 2nd, 3rd, 4th, 5th, 6th
final keyword
find() function, 2nd, 3rd
first-class functions
first-order function
first() function
fixed-size arrays, 2nd, 3rd
floating-point numbers
floating-point types
comparing floating-point numbers
precision values
fmap() function
fold() function, 2nd
forward iterators
function argument types, subtyping and
function keyword
function map
function return types, subtyping and
function types, 2nd
adapter pattern
counters
functional counters
implementing
object-oriented counters
resumable counters
decorator pattern
closures
functional decorator
implementing
functional programming
higher-order functions
filter
library support
map
reduce
lazy values
lambdas
long-running operations
asynchronous execution
synchronous execution
simplifying asynchronous code
async/await
promises
state machines without switch statements
early programming with types
implementing
overview
strategy pattern
first-class functions
implementing
typing functions
functional counters
functional programming, 2nd, 3rd
functors
generic algorithms
adaptive algorithms
common algorithms
higher-order functions
filter
filter/reduce pipeline
map
reduce
implementing fluent pipeline
loops vs.
type parameter constraints
generic algorithms with type constraints
generic data structures with type constraints
using iterators
generic data structures
data structures, defined
decoupling independent concerns
generic types
optional types
reusable identity function
overview
streaming data
processing pipelines
traversing data structures
streamlining iteration code
using iterators
with type parameter constraints
generic function
generic programming, 2nd
generic types
generics
glyphs
grapheme-splitter library
graphemes
generic algorithms
adaptive algorithms
common algorithms
higher-order functions
filter
filter/reduce pipeline
map
reduce
implementing fluent pipeline
loops vs.
type parameter constraints
generic algorithms with type constraints
generic data structures with type constraints
using iterators
generic data structures
data structures, defined
decoupling independent concerns
generic types
optional types
reusable identity function
overview
streaming data
processing pipelines
traversing data structures
streamlining iteration code
using iterators
with type parameter constraints
generic function
generic programming, 2nd
generic types
generics
glyphs
grapheme-splitter library
graphemes
has-a rule of thumb
hash function
hash maps
hash tables
Haskell language, 2nd
heaps
heterogenous collections
base type or interface
sum type or variant
unknown type
higher kinded types
category theory and
dependent types
functional programming
generic programming
linear types
map
functors
mix-and-match function application
processing results or propagating errors
monads
common monads
continuation monad
list monad
map vs. bind, 2nd
monad pattern
result or error
higher-kinded types, 2nd, 3rd
higher-order functions, 2nd
filter
filter/reduce pipeline
generic versions of
library support
map, 2nd
bind vs.
functors
generic versions of
mix-and-match function application
processing results or propagating errors
reduce
filter/reduce pipeline
generic versions of
homogenous collection
has-a rule of thumb
hash function
hash maps
hash tables
Haskell language, 2nd
heaps
heterogenous collections
base type or interface
sum type or variant
unknown type
higher kinded types
category theory and
dependent types
functional programming
generic programming
linear types
map
functors
mix-and-match function application
processing results or propagating errors
monads
common monads
continuation monad
list monad
map vs. bind, 2nd
monad pattern
result or error
higher-kinded types, 2nd, 3rd
higher-order functions, 2nd
filter
filter/reduce pipeline
generic versions of
library support
map, 2nd
bind vs.
functors
generic versions of
mix-and-match function application
processing results or propagating errors
reduce
filter/reduce pipeline
generic versions of
homogenous collection
identities
identity() function, 2nd, 3rd
IEEE 754, 2nd
IForwardIteratorinterface, 2nd
IIncrementable
IInputIteratorinterface
immutability
implicit type cast
inheritance
is-a rule of thumb
modeling hierarchies
parameterizing behavior
input iterators
instance of keyword
integer types
overflow and underflow
interfaces (contracts)
intersection types
invariant types, 2nd
invariants
IOutputIterator
IRandomAccessIterator
IReadable
is keyword
is-a rule of thumb
Iterable argument, 2nd, 3rd
Iterable interface, 2nd, 3rd, 4th
IterableIterator interface, 2nd, 3rd, 4th, 5th, 6th
iteration
Iterator interface, 2nd, 3rd
IteratorResult type, 2nd, 3rd
iterators
algorithms using
bidirectional iterators
forward iterators
iterator building blocks
random-access iterators
defined
streamlining iteration code
traversing data structures using
IWritable interface, 2nd
identities
identity() function, 2nd, 3rd
IEEE 754, 2nd
IForwardIteratorinterface, 2nd
IIncrementable
IInputIteratorinterface
immutability
implicit type cast
inheritance
is-a rule of thumb
modeling hierarchies
parameterizing behavior
input iterators
instance of keyword
integer types
overflow and underflow
interfaces (contracts)
intersection types
invariant types, 2nd
invariants
IOutputIterator
IRandomAccessIterator
IReadable
is keyword
is-a rule of thumb
Iterable argument, 2nd, 3rd
Iterable interface, 2nd, 3rd, 4th
IterableIterator interface, 2nd, 3rd, 4th, 5th, 6th
iteration
Iterator interface, 2nd, 3rd
IteratorResult type, 2nd, 3rd
iterators
algorithms using
bidirectional iterators
forward iterators
iterator building blocks
random-access iterators
defined
streamlining iteration code
traversing data structures using
IWritable interface, 2nd
java.util.stream package
JSON.parse() method, 2nd, 3rd
JSON.stringify() method
java.util.stream package
JSON.parse() method, 2nd, 3rd
JSON.stringify() method
lambdas (anonymous functions)
lazy evaluation, 2nd
lazy values
linear space (O(n))
linear time (O(n))
linear types
linearithmic (O(n log n))
linked lists
LinkedList
Liskov substitution principle
list efficiency
list monad
long-running operations
asynchronous execution
callbacks
models for
synchronous execution
loops, algorithms vs.
lambdas (anonymous functions)
lazy evaluation, 2nd
lazy values
linear space (O(n))
linear time (O(n))
linear types
linearithmic (O(n log n))
linked lists
LinkedList
Liskov substitution principle
list efficiency
list monad
long-running operations
asynchronous execution
callbacks
models for
synchronous execution
loops, algorithms vs.
machine epsilon
mantissa
map() function, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th, 10th, 11th, 12th
bind vs.
functors
generic versions of
mix-and-match function application
processing results or propagating errors
max() function
maybe type
maybe types (optional types), 2nd, 3rd
mix-ins
monadic error handling
monads
common monads
continuation monad
list monad
map vs. bind
monad pattern
result or error
monoids
machine epsilon
mantissa
map() function, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th, 10th, 11th, 12th
bind vs.
functors
generic versions of
mix-and-match function application
processing results or propagating errors
max() function
maybe type
maybe types (optional types), 2nd, 3rd
mix-ins
monadic error handling
monads
common monads
continuation monad
list monad
map vs. bind
monad pattern
result or error
monoids
N-bit unsigned integer
name property, 2nd
namespace keyword
NaN (not a number)
narrowing casts
negative infinity
never type, 2nd, 3rd, 4th, 5th
next elements
next() method, 2nd, 3rd, 4th, 5th
nominal subtyping
pros and cons of
simulating
none() function
nonprimitive types
nonterminating functions
NOT operator
null type, 2nd, 3rd
nullable type
number type, 2nd, 3rd, 4th, 5th, 6th, 7th
Number.isSafeInteger() function
numerical types
arbitrarily large numbers
floating-point types
comparing floating-point numbers
precision values
integer types
overflow and underflow
N-bit unsigned integer
name property, 2nd
namespace keyword
NaN (not a number)
narrowing casts
negative infinity
never type, 2nd, 3rd, 4th, 5th
next elements
next() method, 2nd, 3rd, 4th, 5th
nominal subtyping
pros and cons of
simulating
none() function
nonprimitive types
nonterminating functions
NOT operator
null type, 2nd, 3rd
nullable type
number type, 2nd, 3rd, 4th, 5th, 6th, 7th
Number.isSafeInteger() function
numerical types
arbitrarily large numbers
floating-point types
comparing floating-point numbers
precision values
integer types
overflow and underflow
object type, 2nd, 3rd, 4th
object-oriented counters
OOP (object-oriented programming)
alternatives to
functional programming
generic programming
sum types
composition
composite classes
has-a rule of thumb
implementing
extending behavior
with composition
with mix-ins
inheritance
is-a rule of thumb
modeling hierarchies
parameterizing behavior
interfaces
Optional class
optional type, 2nd
optional types (maybe types), 2nd
Optionaltype, 2nd, 3rd, 4th
OR operator
out keyword
out parameters
overflow
detecting
overview
object type, 2nd, 3rd, 4th
object-oriented counters
OOP (object-oriented programming)
alternatives to
functional programming
generic programming
sum types
composition
composite classes
has-a rule of thumb
implementing
extending behavior
with composition
with mix-ins
inheritance
is-a rule of thumb
modeling hierarchies
parameterizing behavior
interfaces
Optional class
optional type, 2nd
optional types (maybe types), 2nd
Optionaltype, 2nd, 3rd, 4th
OR operator
out keyword
out parameters
overflow
detecting
overview
Pair type
parameterizing behavior
positive infinity
predicates, 2nd, 3rd
primitive obsession antipattern
primitive types, 2nd
private property
private variables
product types, 2nd.
See also compound types.
Promise, 2nd, 3rd
Promise class
Promise.all() function, 2nd
Promise.race() function, 2nd
Promise.resolve() function
Promise.then() function
promises, 2nd, 3rd
chaining
chaining synchronous functions
creating, 2nd
handling errors
proofs
proofs-as-programs
property
pthread_create() function
public property
Pair type
parameterizing behavior
positive infinity
predicates, 2nd, 3rd
primitive obsession antipattern
primitive types, 2nd
private property
private variables
product types, 2nd.
See also compound types.
Promise, 2nd, 3rd
Promise class
Promise.all() function, 2nd
Promise.race() function, 2nd
Promise.resolve() function
Promise.then() function
promises, 2nd, 3rd
chaining
chaining synchronous functions
creating, 2nd
handling errors
proofs
proofs-as-programs
property
pthread_create() function
public property
random-access iterators
read-only variables
readonly properties
record types
reduce() function, 2nd, 3rd, 4th, 5th, 6th
filter/reduce pipeline
generic versions of
reduceRight() method
reference types
references
associative arrays
binary trees
implementation trade-offs
list efficiency
overview
reject() function
rejected state, promise
resolve() function
resumable counters
reusable identity functions
reverse() function, 2nd, 3rd
run time
random-access iterators
read-only variables
readonly properties
record types
reduce() function, 2nd, 3rd, 4th, 5th, 6th
filter/reduce pipeline
generic versions of
reduceRight() method
reference types
references
associative arrays
binary trees
implementation trade-offs
list efficiency
overview
reject() function
rejected state, promise
resolve() function
resumable counters
reusable identity functions
reverse() function, 2nd, 3rd
run time
saturation
sealed keyword
second-order function
security
select() function, 2nd
serialization
settled state, promise
shape-preserving operations
short circuit evaluation
single-responsibility principle
skip() function
state machines, 2nd
early programming with types
implementing
overview
state space
static typing
strategy pattern
first-class functions
function types
implementing
streaming data
strict settings
string type, 2nd, 3rd, 4th
strings
breaking text
encoding libraries
encodings
UTF-16
UTF-32
UTF-8
strong typing
struct type, 2nd
structural subtyping
subtyping, 2nd, 3rd, 4th
collections and
distinguishing between similar types
simulating nominal subtyping
structural vs. nominal subtyping
function argument types and
function return types and
sum types and
types that can be assigned to anything
types to which anything can be assigned
sum types, 5th.
See also either-or types.
as alternative to OOP
heterogenous collections
subtyping and
switch statements, 2nd
synchronous execution
chaining synchronous functions
of long-running operations
System.Linq namespace, 2nd
saturation
sealed keyword
second-order function
security
select() function, 2nd
serialization
settled state, promise
shape-preserving operations
short circuit evaluation
single-responsibility principle
skip() function
state machines, 2nd
early programming with types
implementing
overview
state space
static typing
strategy pattern
first-class functions
function types
implementing
streaming data
strict settings
string type, 2nd, 3rd, 4th
strings
breaking text
encoding libraries
encodings
UTF-16
UTF-32
UTF-8
strong typing
struct type, 2nd
structural subtyping
subtyping, 2nd, 3rd, 4th
collections and
distinguishing between similar types
simulating nominal subtyping
structural vs. nominal subtyping
function argument types and
function return types and
sum types and
types that can be assigned to anything
types to which anything can be assigned
sum types, 5th.
See also either-or types.
as alternative to OOP
heterogenous collections
subtyping and
switch statements, 2nd
synchronous execution
chaining synchronous functions
of long-running operations
System.Linq namespace, 2nd
tagged union types
tagged union types (Variant types)
take() function, 2nd
Task
text-breaking function
then() function, 2nd, 3rd, 4th
third-order function
threads
throw statement
top types
transform() function, 2nd
traversing data structures
streamlining iteration code
using iterators
tuple types
two’s complement encoding
type casting, 2nd, 3rd
common type casts
downcasts
narrowing casts
upcasts
widening casts
overview
tracking types outside type system
type checking
type constructors
type guards
type inference, 2nd
type parameter constraints
generic algorithms with
generic data structures with
type safety
enforcing constraints
with constructor
with factory
hiding and restoring type information
heterogenous collections
serialization
preventing misinterpretation
Mars Climate Orbiter
primitive obsession antipattern
type casting
common type casts
overview
tracking types outside type system
type systems
benefits of
composability
correctness
encapsulation
immutability
readability
data interpretation
defined
purpose of
types of
dynamic typing
static typing
strong typing
type inference
weak typing
types, defined
types
arrays and references
associative arrays
binary trees
fixed-size arrays
implementation trade-offs
list efficiency
references
Boolean types
Boolean expressions
short circuit evaluation
combining
algebraic data types
compound types
either-or types
visitor design pattern
defined
empty types
function types
counters
decorator pattern
higher-order functions
lazy values
long-running operations
simplifying asynchronous code
state machines without switch statements
strategy pattern
higher kinded types
category theory and
dependent types
functional programming
generic programming
linear types
map
monads
numerical types
arbitrarily large numbers
floating-point types
integer types
strings
breaking text
encoding libraries
encodings
unit types
TypeScript, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th, 10th, 11th, 12th, 13th, 14th, 15th, 16th, 17th
cheat sheet
installing locally
online playground
source code
tagged union types
tagged union types (Variant types)
take() function, 2nd
Task
text-breaking function
then() function, 2nd, 3rd, 4th
third-order function
threads
throw statement
top types
transform() function, 2nd
traversing data structures
streamlining iteration code
using iterators
tuple types
two’s complement encoding
type casting, 2nd, 3rd
common type casts
downcasts
narrowing casts
upcasts
widening casts
overview
tracking types outside type system
type checking
type constructors
type guards
type inference, 2nd
type parameter constraints
generic algorithms with
generic data structures with
type safety
enforcing constraints
with constructor
with factory
hiding and restoring type information
heterogenous collections
serialization
preventing misinterpretation
Mars Climate Orbiter
primitive obsession antipattern
type casting
common type casts
overview
tracking types outside type system
type systems
benefits of
composability
correctness
encapsulation
immutability
readability
data interpretation
defined
purpose of
types of
dynamic typing
static typing
strong typing
type inference
weak typing
types, defined
types
arrays and references
associative arrays
binary trees
fixed-size arrays
implementation trade-offs
list efficiency
references
Boolean types
Boolean expressions
short circuit evaluation
combining
algebraic data types
compound types
either-or types
visitor design pattern
defined
empty types
function types
counters
decorator pattern
higher-order functions
lazy values
long-running operations
simplifying asynchronous code
state machines without switch statements
strategy pattern
higher kinded types
category theory and
dependent types
functional programming
generic programming
linear types
map
monads
numerical types
arbitrarily large numbers
floating-point types
integer types
strings
breaking text
encoding libraries
encodings
unit types
TypeScript, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th, 10th, 11th, 12th, 13th, 14th, 15th, 16th, 17th
cheat sheet
installing locally
online playground
source code
undefined type, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th
underflow
detecting
overview
underscore.js package
Unicode, 2nd
uninhabitable types
unique symbol trick, 2nd, 3rd
unit types
unit() function, 2nd
unknown type, 2nd, 3rd, 4th, 5th
upcasts
UTF-16 encoding
UTF-32 encoding
UTF-8 encoding
undefined type, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th
underflow
detecting
overview
underscore.js package
Unicode, 2nd
uninhabitable types
unique symbol trick, 2nd, 3rd
unit types
unit() function, 2nd
unknown type, 2nd, 3rd, 4th, 5th
upcasts
UTF-16 encoding
UTF-32 encoding
UTF-8 encoding
value property, 2nd
value types
variable-length encodings
Variant types (tagged union types), 2nd
visit() function, 2nd, 3rd, 4th
visitor design pattern
alternative implementation of
naïve implementation
naïve implementation of
variant visitor function
void property, 2nd
void type, 2nd, 3rd, 4th, 5th
value property, 2nd
value types
variable-length encodings
Variant types (tagged union types), 2nd
visit() function, 2nd, 3rd, 4th
visitor design pattern
alternative implementation of
naïve implementation
naïve implementation of
variant visitor function
void property, 2nd
void type, 2nd, 3rd, 4th, 5th
weak typing
well-formed values
where() function, 2nd
widening casts, 2nd
wrap around
weak typing
well-formed values
where() function, 2nd
widening casts, 2nd
wrap around
yield keyword
yield statement, 2nd, 3rd
yield keyword
yield statement, 2nd, 3rd
第 1 章类型简介
Chapter 1. Introduction to typing
图 1.2。类型为带符号的 16 位整数的位序列。类型信息(16 位带符号整数)告诉编译器和/或运行时,位序列表示 -32768 和 32767 之间的整数值,确保正确解释为 -15709。
图 1.3。源代码由编译器或解释器转换为可由运行时执行的代码。运行时是物理计算机或虚拟机,比如Java的JVM,或者浏览器的JavaScript引擎。
图 1.4。正确声明类型,我们可以禁止无效值。第一种类型过于宽松,允许我们不想要的值。如果代码试图将不需要的值分配给变量,则第二种限制性更强的类型将无法编译。
Figure 1.1. A sequence of bits can be interpreted in multiple ways.
第 2 章基本类型
Chapter 2. Basic types
图 2.2。4 位无符号整数编码。当所有 4 位都为 0 时,最小可能值是 0。当所有位都为 1 时,最大可能值是 15 (1 * 23 + 1 * 22 + 1 * 21 + 1 * 20)。
图 2.3。4 位有符号整数编码。–8 编码为 24 – 8(二进制 1000),–3 编码为 24 – 3(二进制 1101)。对于负数,第一位始终为 1,对于正数,第一位始终为 0。
图 2.4。处理算术溢出的不同方法。里程表从 999999 回绕到 0;一个旋钮简单地停在可能的最大值;袖珍计算器打印错误并停止。
图 2.6。女警官表情符号的字符编码(警官表情符号字符 + 零宽度连接符 + 女性符号表情符号)和生成的字素(女警官)。
图 2.7。女警官表情符号被视为内存中位的 UTF-16 字符串编码、UTF-16 字节序列、Unicode 代码点序列和字素。
图 2.9。一个基于数组的列表,包含 9 个元素,容量为 16。在必须将数据移动到更大的数组之前,可以附加七个元素。
图 2.10。二叉树表示为固定大小的数组。缺少的节点(2 的右子节点)是数组中未使用的元素。节点之间的父子关系是隐式的,因为可以根据父节点的索引计算子节点的索引,反之亦然。
图 2.11。只有三个节点的稀疏二叉树仍然需要一个包含七个元素的数组才能正确表示。如果节点 9 有一个孩子,数组大小将变为 15。
第三章作文
Chapter 3. Composition
图 3.1。组合两种类型,以便生成的类型包含它们各自的值。每个表情符号代表其中一种类型的值。括号将组合类型的值表示为原始类型的成对值。
图 3.2。对 (1, 5) 的两种解释方式:作为 X 坐标 1 和 Y 坐标 5 的点 A,或作为 X 坐标 5 和 Y 坐标 1 的点 B。
图 3.3。组合两种类型,以便生成的类型允许来自两种类型之一的值。
图 3.4。Result 类型的所有可能值作为 InputError 和 DayOfWeek 的组合。那是 21 个值(3 InputError x 7 DayOfWeek)。
第 4 章类型安全
Chapter 4. Type safety
图 4.1。数值 1000 可以代表 1,000 美元或 1,000 英里。两个不同的开发人员可以将其解释为两种截然不同的措施。
图 4.2。具有明确的货币类型可以清楚地表明该值不代表 1,000 英里,而是代表美元金额。
图 4.3。通过转换,我们可以将 16 位有符号整数类型的值转换为 UTF-8 编码字符。
图 4.4。如果我们有一个三角形或一个正方形,我们不能确定我们拥有的实际形状是否会通过三角形槽。如果它是三角形会,但如果它是正方形则不会。
图 4.5。扩大和缩小铸件的示例。加宽转换是安全的:灰色方块代表我们得到的额外位,因此不会丢失任何信息。另一方面,缩小转换是危险的:黑色方块代表不再适合新类型的位。
图 4.6。如果我们有一个只装猫的袋子,我们可以打赌,无论我们从袋子里拿出什么东西,都会是一只猫。如果袋子里也能装杂货,我们就不能再保证能拿出什么东西了。
第 5 章函数类型
Chapter 5. Function types
图 5.1。策略模式由 IStrategy 接口、ConcreteStrategy1 和 ConcreteStrategy2 实现以及通过 IStrategy 接口使用算法的 Context 组成。
图 5.2。由使用函数的 Context 组成的策略模式:concreteStrategy1() 或 concreteStrategy2()
图 5.3。两个包含代码示例的 TypeScript (.ts) 文件应内联在 Markdown 文档中的 ```ts 和 ``` 标记之间。<!-- ... --> 注释注释了我的脚本的代码示例。
图 5.4。文本处理状态机,具有三种状态(文本处理、标记处理、代码处理)和基于输入的状态之间的转换。文本处理是初始状态或开始状态。
图 5.5。对数字求平方和获取字符串长度是截然不同的场景,但转换的整体结构是相同的:采用输入数组,应用函数,然后生成输出数组。
图 5.6。甚至长度为 5 的数字和字符串共享一个结构。我们遍历输入,应用过滤器,并输出过滤器返回 true 的项目。
图 5.7。组合数字数组中的数字和字符串数组中的字符串的通用结构。在第一种情况下,初始值为 1,我们应用的组合是与每个项目相乘。在第二种情况下,初始值为“”,我们应用的组合是与每个项目的连接。
第 6 章函数类型的高级应用
Chapter 6. Advanced applications of function types
图 6.1。装饰器模式:一个 IComponent 接口,一个通过 ConcreteComponent 的具体实现,以及一个通过附加行为增强 IComponent 的装饰器
图 6.3。函数式装饰器:我们现在只有一个 makeWidget() 函数和一个 singletonDecorator() 函数。
图 6.4。一个返回闭包的简单函数:一个引用函数局部变量的 lambda。即使在 getClosure() 返回之后,这个变量仍然被闭包引用,所以它比它出现的函数还长。
图 6.6。createThread() 创建一个新线程。原线程继续执行operation1(),然后operation2(),新线程并行执行longRunningOperation()。
第 7 章子类型化
Chapter 7. Subtyping
图 7.1。顶级类型是任何其他类型的超类型。我们可以定义任意数量的类型,但它们中的任何一个都将是顶级类型的子类型。我们可以在需要顶级类型的地方使用任何类型的值。
图 7.2。底层类型是任何其他类型的子类型。我们可以定义任意数量的类型,但其中任何一个都将是底层类型的超类型。我们可以在任何需要任何类型的值的地方传递一个底层类型的值(尽管我们永远不能产生这样的值)。
图 7.3。三角形 | 正方形是三角形的子类型 | 方形 | Circle 因为只要需要三角形、正方形或圆形,我们就可以使用三角形或正方形。
第 8 章面向对象编程的要素
Chapter 8. Elements of object-oriented programming
图 8.1。所有的动物都吃。我们可以和宠物一起玩(但它们仍然需要吃饭)。猫也会喵喵叫(但它们仍然会玩耍和吃东西)。
图 8.2。以 BinaryExpression 作为父项,SumExpression 和 MulExpression 作为子项的表达式层次结构
图 8.3。所有形状都有一个 ID。圆是一种形状,所以它继承了id。圆有一个定义其中心的点。
图 8.5。使用 WildAnimal 和 Wolf 扩展动物等级。野生动物可以 roam(),狼可以使用其 track()、stalk() 和 pounce() 方法狩猎。
图 8.6。Hunter 类型是 Wolf 和 Tiger 的父类,提供狩猎行为。
图 8.8。Cat、Wolf 和 Tiger 混合在 HunterBehavior 中,它删除了一堆样板:这些类不再需要包装 HunterBehavior 对象并转发调用。他们可以简单地包括行为。
图 8.9。面向对象的策略模式。在 ConcreteStrategy1 和 ConcreteStrategy2 中实现了不同版本的算法。
Figure 8.6. The Hunter type is the parent of Wolf and Tiger, and provides hunting behavior.
第 9 章通用数据结构
Chapter 9. Generic data structures
Figure 9.3. Binary tree example
第 10 章通用算法和迭代器
Chapter 10. Generic algorithms and iterators
图 10.1。用堆栈反转序列:原始序列中的元素被压入堆栈,然后弹出以产生反转序列。
图 10.3。begin 和 end 迭代器定义一个范围:begin 指向第一个元素,end 指向最后一个元素。
图 10.4。输入迭代器可以检索当前元素的值并前进到下一个元素。
图 10.5。前向迭代器可以读取和写入当前元素的值,前进到下一个元素,并创建一个支持多次遍历的自身克隆。在此图中,我们看到 clone() 如何创建迭代器的副本。当我们推进原件时,克隆件不会移动。
Figure 10.2. Reversing an array in place by swapping its elements
第 11 章高等类型及更高类型
Chapter 11. Higher kinded types and beyond
图 11.1。map() 接受一个序列的迭代器,在本例中是一个圆列表,以及一个转换圆的函数。map() 将函数应用于序列中的每个元素,并生成一个包含转换元素的新序列。
图 11.2。将函数映射到可选值。如果可选为空,map() 返回一个空的可选;否则,它将函数应用于值并返回包含结果的可选。
图 11.3。将函数映射到 Box 中的值。map() 从 Box 中解压值,应用函数,然后将值放回 Box 中。
图 11.8。List monad:bind() 接受一个 Ts 序列(本例中为白色圆圈)和一个函数 T => Us 序列(本例中为黑色圆圈)。结果是我们的扁平列表(黑色圆圈)。
第 2 章基本类型
Chapter 2. Basic types
Table 2.1. Detecting integer overflow for a and b in a [MIN, MAX] range with MIN = –MAX-1
Table 2.2. Detecting integer underflow for a and b in [MIN, MAX] range with MIN = –MAX-1
第 4 章类型安全
Chapter 4. Type safety
Table 4.1. Pros and cons of heterogenous list implementations
附录 B. TypeScript 备忘单
Appendix B. TypeScript cheat sheet
第 1 章类型简介
Chapter 1. Introduction to typing
Listing 1.1. Trying to interpret data as code
Listing 1.2. Insufficient type information
Listing 1.3. Refined type information
Listing 1.6. Not enough encapsulation
Listing 1.8. Noncomposable system
Listing 1.9. Noncomposable system update
第 2 章基本类型
Chapter 2. Basic types
Listing 2.1. Raising and logging an error if a config file is not found
Listing 2.2. Empty type implemented as an uninstantiable class
Listing 2.3. A “Hello world!” function
Listing 2.4. Unit type implemented as a singleton without state
Listing 2.6. Alternative gatekeeper implementation
Listing 2.7. Function adding up item total
Listing 2.8. Checking for addition overflow
Listing 2.9. Currency class and currency addition function
Listing 2.10. Floating-point equality within epsilon
Listing 2.11. Simple text-breaking function
Listing 2.12. Text-breaking function using grapheme-splitter library
Listing 2.13. Linked-list implementation
Listing 2.14. Array-based list implementation
Listing 2.15. Array-based list implementation with additional capacity
第三章作文
Chapter 3. Composition
Listing 3.1. Distance between two points
Listing 3.2. Distance between two points defined as tuples
Listing 3.4. Distance between two points defined as records
Listing 3.5. Ill-formed currency
Listing 3.6. Currency maintaining invariants
Listing 3.7. Immutable Currency
Listing 3.8. Encoding day of week as a number
Listing 3.9. Encoding day of week with constants
Listing 3.10. Encoding day of week as an enum
Listing 3.11. Parsing input into a DayOfWeek or undefined
Listing 3.13. Returning result and error from a function
Listing 3.15. Returning result or error from a function
Listing 3.16. Tagged union of shapes
Listing 3.18. Union of shapes as variant
Listing 3.19. Naïve implementation
第 4 章类型安全
Chapter 4. Type safety
Listing 4.1. Sketch of incompatible components
Listing 4.2. Pound-force second and Newton-second types
Listing 4.3. Converting lbfs to Ns
Listing 4.4. Updated components
Listing 4.5. Constructor throwing on invalid value
Listing 4.6. Constructor coercing an invalid value
Listing 4.7. Factory returning undefined on invalid value
Listing 4.8. Type cast causing a run-time error
Listing 4.9. Revisiting Either implementation
Listing 4.10. makeLeft and makeRight
Listing 4.11. Triangle or Square
Listing 4.12. isLeft() and getLeft()
Listing 4.15. A collection of types implementing IDocumentItem
Listing 4.16. A collection of types as a sum type
Listing 4.17. A collection of unknown type
Listing 4.18. Serializing a cat
Listing 4.19. Deserializing a Cat
第 5 章函数类型
Chapter 5. Function types
Listing 5.1. Car-wash strategy
Listing 5.2. Car-wash strategy revisited
Listing 5.3. Pluggable Greeter
Listing 5.6. State machine implementation
Listing 5.7. Alternative state machine implementation
Listing 5.8. Eager Car production
Listing 5.9. Lazy Car production
Listing 5.10. Anonymous Car production
第 6 章函数类型的高级应用
Chapter 6. Advanced applications of function types
清单 6.21。使用 Promise.all() 来排序执行
Listing 6.1. WidgetFactory decorator
Listing 6.2. Functional widget factory
Listing 6.3. Functional widget factory decorator
Listing 6.4. Decorator function
Listing 6.6. Object-oriented counter
Listing 6.7. Functional counter
Listing 6.8. Resumable counter
Listing 6.9. Synchronous execution
Listing 6.10. Asynchronous execution with callback
Listing 6.11. Counting down in an event loop
Listing 6.12. Two counters in an event loop
Listing 6.13. Counter with callback
Listing 6.14. Chaining callbacks
Listing 6.15. Functions returning promises
Listing 6.16. Chaining promises
Listing 6.17. getUserName() returning a promise
Listing 6.18. Rejecting a promise
Listing 6.19. Chaining functions returning promises
Listing 6.20. Chaining functions that don’t return promises
Listing 6.21. Using Promise.all() to sequence execution
Listing 6.22. Using Promise.race() to sequence execution
第 7 章子类型化
Chapter 7. Subtyping
清单 7.11。turnAgain() 使用 fail() 并返回一个虚拟值
清单 7.12。turnAngle() 使用 fail() 并返回其结果
清单 7.13。三角形 | 正方形变三角形 | 方形 | 圆圈
清单 7.14。三角形 | 方形 | 圆为三角形 | 正方形
清单 7.17。LinkedList<Triangle> 作为 LinkedList<Shape>
Listing 7.1. Pound-force second and Newton-second types
Listing 7.2. Pound-force second and Newton-second without unique symbols
Listing 7.3. User is structurally a subtype of Named
Listing 7.4. Simulating nominal subtyping
Listing 7.5. Deserializing any
Listing 7.6. Run-time type checking for User
Listing 7.7. Stronger typing using unknown
Listing 7.8. TurnDirection to angle conversion
Listing 7.10. turnAngle() using fail()
Listing 7.11. turnAgain() using fail() and returning a dummy value
Listing 7.12. turnAngle() using fail() and returning its result
Listing 7.13. Triangle | Square as Triangle | Square | Circle
Listing 7.14. Triangle | Square | Circle as Triangle | Square
Listing 7.15. EquilateralTriangle declaration
Listing 7.16. Triangle[] as Shape[]
Listing 7.17. LinkedList<Triangle> as LinkedList<Shape>
Listing 7.18. () => Triangle as () => Shape
Listing 7.19. () => Shape as () => Triangle
Listing 7.20. Overriding a method with a subtype as return type
Listing 7.21. Draw and render functions
Listing 7.22. Shape and Triangle with isRightAngled() method
Listing 7.23. Updated draw and render functions
Listing 7.24. Attempting to call isRightAngled() on a supertype of Triangle
第 8 章面向对象编程的要素
Chapter 8. Elements of object-oriented programming
Listing 8.3. Extended logger interface
Listing 8.4. Combining interfaces
Listing 8.6. Expression hierarchy
Listing 8.7. Inheritance and composition
Listing 8.11. Hunting behavior
Listing 8.12. Extending an instance with the members of another one
Listing 8.13. Mixing in behavior
Listing 8.14. Visitor with OOP
第 9 章通用数据结构
Chapter 9. Generic data structures
清单 9.5。doNothing() 和 pluckAll()
清单 9.27。带有 Iterator 参数的 print() 和 contains()
Listing 9.2. Default transform()
Listing 9.3. assembleWidgets()
Listing 9.5. doNothing() and pluckAll()
Listing 9.7. Unsafe use of any
Listing 9.11. Binary tree of numbers
Listing 9.12. Linked list of strings
Listing 9.13. Generic binary tree
Listing 9.14. Generic linked list
Listing 9.16. printInOrder() example
Listing 9.17. Print linked list
Listing 9.18. printLinkedList() example
Listing 9.20. Iterator interface
Listing 9.21. Binary tree iterator
Listing 9.22. Linked list iterator
Listing 9.23. print() using iterator
Listing 9.24. contains() using iterator
Listing 9.25. Iterable interface
Listing 9.26. Iterable linked list
Listing 9.27. print() and contains() with Iterator argument
Listing 9.28. print() and contains() with Iterable argument
Listing 9.29. Binary tree iterator
Listing 9.30. Binary tree iterator using generator
Listing 9.31. Linked list iterator
Listing 9.32. Linked list iterator using generator
Listing 9.33. Iterable linked list using generator
Listing 9.34. Inifinite stream of random numbers
第 10 章通用算法和迭代器
Chapter 10. Generic algorithms and iterators
清单 10.17。带有 compare() 参数的 max() 算法
清单 10.20。IReadable<T> 和 IIncrementable<T>
清单 10.25。IWritable<T> 和 IOutputIterator<T>
清单 10.30。LinkedListIterator<T> 实现 IForwardIterator<T>
清单 10.33。IBidirectionalIterator<T> 和 ArrayIterator<T>
清单 10.36。IRandomAccessIterator<T>
清单 10.37。ArrayIterator<T> 实现随机访问迭代器
Listing 10.2. map() with iterator
Listing 10.4. filter() with iterator
Listing 10.6. reduce() with iterator
Listing 10.7. filter()/reduce() pipeline
Listing 10.8. filter/reduce pipeline
Listing 10.10. Fluent filter/reduce pipeline
Listing 10.11. Better fluent iterable
Listing 10.12. Better fluent filter/reduce pipeline
Listing 10.13. renderAll sketch
Listing 10.14. renderAll with constraint
Listing 10.15. IComparable interface
Listing 10.16. max() algorithm
Listing 10.17. max() algorithm with compare() argument
Listing 10.18. reverse() with stack
Listing 10.19. reverse() for array
Listing 10.20. IReadable<T> and IIncrementable<T>
Listing 10.21. IInputIterator<T>
Listing 10.22. Linked list implementation
Listing 10.23. Linked list input iterator
Listing 10.24. Pair of iterators over linked list
Listing 10.25. IWritable<T> and IOutputIterator<T>
Listing 10.26. Console output iterator
Listing 10.27. map() with input and output iterators
Listing 10.28. find() with iterable
Listing 10.29. IForwardIterator<T>
Listing 10.30. LinkedListIterator<T> implementing IForwardIterator<T>
Listing 10.31. find() with forward iterator
Listing 10.32. Replacing 42 with 0 in a linked list
Listing 10.33. IBidirectionalIterator<T> and ArrayIterator<T>
Listing 10.34. reverse() with bidirectional iterator
Listing 10.35. Reversing an array of numbers
Listing 10.36. IRandomAccessIterator<T>
Listing 10.37. ArrayIterator<T> implementing a random-access iterator
Listing 10.39. elementAt() with input and random-access iterators
第 11 章高等类型及更高类型
Chapter 11. Higher kinded types and beyond
清单 11.7。square() 和 stringify()
Listing 11.7. square() and stringify()
Listing 11.8. readNumber() return type
Listing 11.9. Processing a number
Listing 11.10. Processing with map()
Listing 11.11. Processing with lambda
Listing 11.12. Unpacking values for square()
Listing 11.13. Unpacking values for stringify()
Listing 11.15. Sketch of Functor interface
Listing 11.16. Box implementing the interface
Listing 11.17. Functor interface
Listing 11.19. Applying map() over a function
Listing 11.20. Functions returning result or error
Listing 11.22. Processing and explicitly checking for errors
Listing 11.24. Incompatible types
Listing 11.26. Branchless readCatFromFile()